diff --git a/phpstan.neon b/phpstan.neon index c1a0e56e..d5dc4ef6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,7 +4,7 @@ includes: - vendor/thecodingmachine/phpstan-strict-rules/phpstan-strict-rules.neon parameters: - level: 5 + level: max paths: - src diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php index c23e9bf0..12486f50 100644 --- a/src/Bootstrappers/BootSubscribers.php +++ b/src/Bootstrappers/BootSubscribers.php @@ -15,7 +15,7 @@ final class BootSubscribers /** * The Kernel subscribers. * - * @var array + * @var array> */ private static array $subscribers = [ Subscribers\EnsureConfigurationIsValid::class, diff --git a/src/Datasets.php b/src/Datasets.php index 429de157..248b1d38 100644 --- a/src/Datasets.php +++ b/src/Datasets.php @@ -7,7 +7,9 @@ namespace Pest; use Closure; use Pest\Exceptions\DatasetAlreadyExist; use Pest\Exceptions\DatasetDoesNotExist; +use Pest\Exceptions\ShouldNotHappen; use SebastianBergmann\Exporter\Exporter; +use function sprintf; use Traversable; /** @@ -18,14 +20,14 @@ final class Datasets /** * Holds the datasets. * - * @var array> + * @var array> */ private static array $datasets = []; /** * Holds the withs. * - * @var array + * @var array|string>> */ private static array $withs = []; @@ -44,23 +46,31 @@ final class Datasets } /** - * Sets the given. + * Sets the given "with". * - * @param Closure|iterable|string $with + * @param array|string> $with */ - public static function with(string $filename, string $description, Closure|iterable|string $with): void + public static function with(string $filename, string $description, array $with): void { self::$withs[$filename . '>>>' . $description] = $with; } /** - * @return Closure|iterable + * @return Closure|iterable|never + * + * @throws ShouldNotHappen */ public static function get(string $filename, string $description): Closure|iterable { $dataset = self::$withs[$filename . '>>>' . $description]; - return self::resolve($description, $dataset); + $dataset = self::resolve($description, $dataset); + + if ($dataset === null) { + throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); + } + + return $dataset; } /** @@ -79,38 +89,40 @@ final class Datasets $dataset = self::processDatasets($dataset); - $datasetCombinations = self::getDataSetsCombinations($dataset); + $datasetCombinations = self::getDatasetsCombinations($dataset); - $dataSetDescriptions = []; - $dataSetValues = []; + $datasetDescriptions = []; + $datasetValues = []; foreach ($datasetCombinations as $datasetCombination) { $partialDescriptions = []; $values = []; - foreach ($datasetCombination as $dataset_data) { - $partialDescriptions[] = $dataset_data['label']; - $values = array_merge($values, $dataset_data['values']); + foreach ($datasetCombination as $datasetCombinationElement) { + $partialDescriptions[] = $datasetCombinationElement['label']; + + // @phpstan-ignore-next-line + $values = array_merge($values, $datasetCombinationElement['values']); } - $dataSetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions); - $dataSetValues[] = $values; + $datasetDescriptions[] = $description . ' with ' . implode(' / ', $partialDescriptions); + $datasetValues[] = $values; } - foreach (array_count_values($dataSetDescriptions) as $descriptionToCheck => $count) { + foreach (array_count_values($datasetDescriptions) as $descriptionToCheck => $count) { if ($count > 1) { $index = 1; - foreach ($dataSetDescriptions as $i => $dataSetDescription) { - if ($dataSetDescription === $descriptionToCheck) { - $dataSetDescriptions[$i] .= sprintf(' #%d', $index++); + foreach ($datasetDescriptions as $i => $datasetDescription) { + if ($datasetDescription === $descriptionToCheck) { + $datasetDescriptions[$i] .= sprintf(' #%d', $index++); } } } } $namedData = []; - foreach ($dataSetDescriptions as $i => $dataSetDescription) { - $namedData[$dataSetDescription] = $dataSetValues[$i]; + foreach ($datasetDescriptions as $i => $datasetDescription) { + $namedData[$datasetDescription] = $datasetValues[$i]; } return $namedData; @@ -119,7 +131,7 @@ final class Datasets /** * @param array|string> $datasets * - * @return array + * @return array> */ private static function processDatasets(array $datasets): array { @@ -144,10 +156,11 @@ final class Datasets $datasets[$index] = iterator_to_array($datasets[$index]); } + //@phpstan-ignore-next-line foreach ($datasets[$index] as $key => $values) { $values = is_array($values) ? $values : [$values]; $processedDataset[] = [ - 'label' => self::getDataSetDescription($key, $values), + 'label' => self::getDatasetDescription($key, $values), //@phpstan-ignore-line 'values' => $values, ]; } @@ -159,11 +172,11 @@ final class Datasets } /** - * @param array $combinations + * @param array> $combinations * - * @return array + * @return array>> */ - private static function getDataSetsCombinations(array $combinations): array + private static function getDatasetsCombinations(array $combinations): array { $result = [[]]; foreach ($combinations as $index => $values) { @@ -176,20 +189,21 @@ final class Datasets $result = $tmp; } + //@phpstan-ignore-next-line return $result; } /** * @param array $data */ - private static function getDataSetDescription(int|string $key, array $data): string + private static function getDatasetDescription(int|string $key, array $data): string { $exporter = new Exporter(); if (is_int($key)) { - return \sprintf('(%s)', $exporter->shortenedRecursiveExport($data)); + return sprintf('(%s)', $exporter->shortenedRecursiveExport($data)); } - return \sprintf('data set "%s"', $key); + return sprintf('data set "%s"', $key); } } diff --git a/src/Exceptions/InvalidExpectationValue.php b/src/Exceptions/InvalidExpectationValue.php new file mode 100644 index 00000000..68a15c64 --- /dev/null +++ b/src/Exceptions/InvalidExpectationValue.php @@ -0,0 +1,23 @@ +value)) { + InvalidExpectationValue::expected('string'); + } + return $this->toBeJson()->and(json_decode($this->value, true)); } @@ -355,8 +360,12 @@ final class Expectation { foreach ($needles as $needle) { if (is_string($this->value)) { - Assert::assertStringContainsString($needle, $this->value); + // @phpstan-ignore-next-line + Assert::assertStringContainsString((string) $needle, $this->value); } else { + if (!is_iterable($this->value)) { + InvalidExpectationValue::expected('iterable'); + } Assert::assertContains($needle, $this->value); } } @@ -371,6 +380,10 @@ final class Expectation */ public function toStartWith(string $expected): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertStringStartsWith($expected, $this->value); return $this; @@ -383,6 +396,10 @@ final class Expectation */ public function toEndWith(string $expected): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertStringEndsWith($expected, $this->value); return $this; @@ -423,6 +440,10 @@ final class Expectation */ public function toHaveCount(int $count): Expectation { + if (!is_countable($this->value) && !is_iterable($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertCount($count, $this->value); return $this; @@ -435,6 +456,7 @@ final class Expectation { $this->toBeObject(); + //@phpstan-ignore-next-line Assert::assertTrue(property_exists($this->value, $name)); if (func_num_args() > 1) { @@ -646,6 +668,8 @@ final class Expectation public function toBeJson(): Expectation { Assert::assertIsString($this->value); + + //@phpstan-ignore-next-line Assert::assertJson($this->value); return $this; @@ -716,6 +740,10 @@ final class Expectation */ public function toBeDirectory(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryExists($this->value); return $this; @@ -726,6 +754,10 @@ final class Expectation */ public function toBeReadableDirectory(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryIsReadable($this->value); return $this; @@ -736,6 +768,10 @@ final class Expectation */ public function toBeWritableDirectory(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertDirectoryIsWritable($this->value); return $this; @@ -746,6 +782,10 @@ final class Expectation */ public function toBeFile(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertFileExists($this->value); return $this; @@ -756,6 +796,10 @@ final class Expectation */ public function toBeReadableFile(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } + Assert::assertFileIsReadable($this->value); return $this; @@ -766,6 +810,9 @@ final class Expectation */ public function toBeWritableFile(): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } Assert::assertFileIsWritable($this->value); return $this; @@ -810,6 +857,10 @@ final class Expectation public function toMatchObject(iterable|object $object): Expectation { foreach ((array) $object as $property => $value) { + if (!is_object($this->value) && !is_string($this->value)) { + InvalidExpectationValue::expected('object|string'); + } + Assert::assertTrue(property_exists($this->value, $property)); /* @phpstan-ignore-next-line */ @@ -833,6 +884,9 @@ final class Expectation */ public function toMatch(string $expression): Expectation { + if (!is_string($this->value)) { + InvalidExpectationValue::expected('string'); + } Assert::assertMatchesRegularExpression($expression, $this->value); return $this; @@ -892,10 +946,10 @@ final class Expectation } if (!class_exists($exception)) { - throw new ExpectationFailedException("Exception with message \"{$exception}\" not thrown."); + throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); } - throw new ExpectationFailedException("Exception \"{$exception}\" not thrown."); + throw new ExpectationFailedException("Exception \"$exception\" not thrown."); } /** @@ -920,7 +974,7 @@ final class Expectation */ public function __call(string $method, array $parameters) { - if (!static::hasExtend($method)) { + if (!Expectation::hasExtend($method)) { /* @phpstan-ignore-next-line */ return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } @@ -934,7 +988,8 @@ final class Expectation */ public function __get(string $name): Expectation|OppositeExpectation|Each|HigherOrderExpectation { - if (!method_exists($this, $name) && !static::hasExtend($name)) { + if (!method_exists($this, $name) && !Expectation::hasExtend($name)) { + //@phpstan-ignore-next-line return new HigherOrderExpectation($this, $this->retrieve($name, $this->value)); } diff --git a/src/Factories/Annotations/Depends.php b/src/Factories/Annotations/Depends.php index 15b29359..f41076d9 100644 --- a/src/Factories/Annotations/Depends.php +++ b/src/Factories/Annotations/Depends.php @@ -14,8 +14,12 @@ final class Depends { /** * Adds annotations regarding the "depends" feature. + * + * @param array $annotations + * + * @return array */ - public function add(TestCaseMethodFactory $method, array $annotations): array + public function __invoke(TestCaseMethodFactory $method, array $annotations): array { foreach ($method->depends as $depend) { $depend = Str::evaluable($depend); diff --git a/src/Factories/Annotations/Groups.php b/src/Factories/Annotations/Groups.php index 96752d6e..0876ff5c 100644 --- a/src/Factories/Annotations/Groups.php +++ b/src/Factories/Annotations/Groups.php @@ -13,8 +13,12 @@ final class Groups { /** * Adds annotations regarding the "groups" feature. + * + * @param array $annotations + * + * @return array */ - public function add(TestCaseMethodFactory $method, array $annotations): array + public function __invoke(TestCaseMethodFactory $method, array $annotations): array { foreach ($method->groups as $group) { $annotations[] = "@group $group"; diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index d21b5513..f6a61ba2 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -73,9 +73,9 @@ final class TestCaseFactory { $methodsUsingOnly = $this->methodsUsingOnly(); - $methods = array_filter($this->methods, function ($method) use ($methodsUsingOnly) { + $methods = array_values(array_filter($this->methods, function ($method) use ($methodsUsingOnly) { return count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true); - }); + })); if (count($methods) > 0) { $this->evaluate($this->filename, $methods); @@ -98,6 +98,8 @@ final class TestCaseFactory /** * Creates a Test Case class using a runtime evaluate. + * + * @param array $methods */ public function evaluate(string $filename, array $methods): string { @@ -140,13 +142,18 @@ final class TestCaseFactory } $methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string { + if ($method->description === null) { + throw ShouldNotHappen::fromMessage('The test description may not be empty.'); + } + $methodName = Str::evaluable($method->description); $datasetsCode = ''; $annotations = ['@test']; foreach (self::$annotations as $annotation) { - $annotations = (new $annotation())->add($method, $annotations); + /** @phpstan-ignore-next-line */ + $annotations = (new $annotation())->__invoke($method, $annotations); } if (count($method->datasets) > 0) { @@ -221,6 +228,10 @@ EOF; } if (!$method->receivesArguments()) { + if ($method->closure === null) { + throw ShouldNotHappen::fromMessage('The test closure may not be empty.'); + } + $arguments = Reflection::getFunctionArguments($method->closure); if (count($arguments) > 0) { @@ -237,6 +248,10 @@ EOF; public function getMethod(string $methodName): TestCaseMethodFactory { foreach ($this->methods as $method) { + if ($method->description === null) { + throw ShouldNotHappen::fromMessage('The test description may not be empty.'); + } + if (Str::evaluable($method->description) === $methodName) { return $method; } diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php index ffa4cb4b..540f2677 100644 --- a/src/Factories/TestCaseMethodFactory.php +++ b/src/Factories/TestCaseMethodFactory.php @@ -89,7 +89,7 @@ final class TestCaseMethodFactory $testCase->chains->chain($this); $method->chains->chain($this); - return call_user_func(Closure::bind($closure, $this, $this::class), ...func_get_args()); + return \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); }; } diff --git a/src/Functions.php b/src/Functions.php index 1e306999..fe1dd39a 100644 --- a/src/Functions.php +++ b/src/Functions.php @@ -70,12 +70,14 @@ if (!function_exists('uses')) { /** * The uses function binds the given * arguments to test closures. + * + * @param class-string ...$classAndTraits */ function uses(string ...$classAndTraits): UsesCall { $filename = Backtrace::file(); - return new UsesCall($filename, $classAndTraits); + return new UsesCall($filename, array_values($classAndTraits)); } } @@ -111,7 +113,10 @@ if (!function_exists('it')) { { $description = sprintf('it %s', $description); - return test($description, $closure); + /** @var TestCall $test */ + $test = test($description, $closure); + + return $test; } } diff --git a/src/HigherOrderExpectation.php b/src/HigherOrderExpectation.php index 839f3311..eb959cd4 100644 --- a/src/HigherOrderExpectation.php +++ b/src/HigherOrderExpectation.php @@ -80,7 +80,10 @@ final class HigherOrderExpectation } if (!$this->expectationHasMethod($name)) { - return new self($this->original, $this->retrieve($name, $this->getValue())); + /** @var array|object $value */ + $value = $this->getValue(); + + return new self($this->original, $this->retrieve($name, $value)); } return $this->performAssertion($name, []); diff --git a/src/Kernel.php b/src/Kernel.php index af2d82e6..67c4dcf9 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -37,6 +37,7 @@ final class Kernel public static function boot(): self { foreach (self::$bootstrappers as $bootstrapper) { + //@phpstan-ignore-next-line (new $bootstrapper())->__invoke(); } diff --git a/src/OppositeExpectation.php b/src/OppositeExpectation.php index e4ec3d85..da9ec6ab 100644 --- a/src/OppositeExpectation.php +++ b/src/OppositeExpectation.php @@ -32,7 +32,7 @@ final class OppositeExpectation foreach ($keys as $key) { try { $this->original->toHaveKey($key); - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException $exception) { continue; } @@ -54,7 +54,7 @@ final class OppositeExpectation try { /* @phpstan-ignore-next-line */ $this->original->{$name}(...$arguments); - } catch (ExpectationFailedException) { + } catch (ExpectationFailedException $exception) { return $this->original; } @@ -70,7 +70,7 @@ final class OppositeExpectation { try { $this->original->{$name}; // @phpstan-ignore-line - } catch (ExpectationFailedException) { // @phpstan-ignore-line + } catch (ExpectationFailedException $exception) { // @phpstan-ignore-line return $this->original; } diff --git a/src/PendingCalls/UsesCall.php b/src/PendingCalls/UsesCall.php index 6609e8b2..cb3fdc8c 100644 --- a/src/PendingCalls/UsesCall.php +++ b/src/PendingCalls/UsesCall.php @@ -89,7 +89,7 @@ final class UsesCall */ public function group(string ...$groups): UsesCall { - $this->groups = $groups; + $this->groups = array_values($groups); return $this; } diff --git a/src/Plugin.php b/src/Plugin.php index 9de1f707..5f676be4 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -18,6 +18,8 @@ final class Plugin /** * Lazy loads an `uses` call on the context of plugins. + * + * @param class-string ...$traits */ public static function uses(string ...$traits): void { diff --git a/src/Plugins/Coverage.php b/src/Plugins/Coverage.php index 8febde52..9cf45f3b 100644 --- a/src/Plugins/Coverage.php +++ b/src/Plugins/Coverage.php @@ -80,7 +80,10 @@ final class Coverage implements AddsOutput, HandlesArguments } if ($input->getOption(self::MIN_OPTION) !== null) { - $this->coverageMin = (float) $input->getOption(self::MIN_OPTION); + /** @var int|float $minOption */ + $minOption = $input->getOption(self::MIN_OPTION); + + $this->coverageMin = (float) $minOption; } return $originals; diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php index b8e2b53e..869d1000 100644 --- a/src/Repositories/TestRepository.php +++ b/src/Repositories/TestRepository.php @@ -23,7 +23,7 @@ final class TestRepository private array $testCases = []; /** - * @var array>> + * @var array, 1: array, 2: array}> */ private array $uses = []; @@ -80,7 +80,7 @@ final class TestRepository } } - public function get($filename): TestCaseFactory + public function get(string $filename): TestCaseFactory { return $this->testCases[$filename]; } diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 3fbe0e61..922110e1 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -11,6 +11,8 @@ final class Arr { /** * Checks if the given array has the given key. + * + * @param array $array */ public static function has(array $array, string|int $key): bool { @@ -33,6 +35,8 @@ final class Arr /** * Gets the given key value. + * + * @param array $array */ public static function get(array $array, string|int $key, mixed $default = null): mixed { diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php index 8a136682..5c92e6fd 100644 --- a/src/Support/ChainableClosure.php +++ b/src/Support/ChainableClosure.php @@ -22,8 +22,8 @@ final class ChainableClosure throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.'); } - call_user_func_array(Closure::bind($closure, $this, $this::class), func_get_args()); - call_user_func_array(Closure::bind($next, $this, $this::class), func_get_args()); + \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args()); + \Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args()); }; } @@ -33,8 +33,8 @@ final class ChainableClosure public static function fromStatic(Closure $closure, Closure $next): Closure { return static function () use ($closure, $next): void { - call_user_func_array(Closure::bind($closure, null, self::class), func_get_args()); - call_user_func_array(Closure::bind($next, null, self::class), func_get_args()); + \Pest\Support\Closure::bind($closure, null, self::class)(...func_get_args()); + \Pest\Support\Closure::bind($next, null, self::class)(...func_get_args()); }; } } diff --git a/src/Support/Closure.php b/src/Support/Closure.php new file mode 100644 index 00000000..decb9b71 --- /dev/null +++ b/src/Support/Closure.php @@ -0,0 +1,36 @@ +instances)) { - return $this->instances[$id]; + if (!array_key_exists($id, $this->instances)) { + $this->instances[$id] = $this->build($id); } - $this->instances[$id] = $this->build($id); - return $this->instances[$id]; } @@ -60,6 +60,8 @@ final class Container /** * Tries to build the given instance. + * + * @param class-string $id */ private function build(string $id): object { @@ -83,6 +85,7 @@ final class Container } } + //@phpstan-ignore-next-line return $this->get($candidate); }, $constructor->getParameters() diff --git a/src/Support/ExceptionTrace.php b/src/Support/ExceptionTrace.php index ec17afc8..2e6d53f4 100644 --- a/src/Support/ExceptionTrace.php +++ b/src/Support/ExceptionTrace.php @@ -50,6 +50,8 @@ final class ExceptionTrace $property = new ReflectionProperty($t, 'serializableTrace'); $property->setAccessible(true); + + /** @var array> $trace */ $trace = $property->getValue($t); $cleanedTrace = []; diff --git a/src/Support/HigherOrderCallables.php b/src/Support/HigherOrderCallables.php index 1637ccb2..c6533929 100644 --- a/src/Support/HigherOrderCallables.php +++ b/src/Support/HigherOrderCallables.php @@ -25,13 +25,16 @@ final class HigherOrderCallables * * Create a new expectation. Callable values will be executed prior to returning the new expectation. * - * @param (callable():TValue)|TValue $value + * @param (Closure():TValue)|TValue $value * * @return Expectation */ public function expect(mixed $value): Expectation { - return new Expectation($value instanceof Closure ? Reflection::bindCallableWithData($value) : $value); + /** @var TValue $value */ + $value = $value instanceof Closure ? Reflection::bindCallableWithData($value) : $value; + + return new Expectation($value); } /** diff --git a/src/Support/HigherOrderMessage.php b/src/Support/HigherOrderMessage.php index f756c64d..575eaf98 100644 --- a/src/Support/HigherOrderMessage.php +++ b/src/Support/HigherOrderMessage.php @@ -77,7 +77,7 @@ final class HigherOrderMessage */ public function when(callable $condition): self { - $this->condition = $condition; + $this->condition = Closure::fromCallable($condition); return $this; } diff --git a/src/Support/HigherOrderMessageCollection.php b/src/Support/HigherOrderMessageCollection.php index cb69a3a9..54bef82a 100644 --- a/src/Support/HigherOrderMessageCollection.php +++ b/src/Support/HigherOrderMessageCollection.php @@ -40,6 +40,7 @@ final class HigherOrderMessageCollection public function chain(object $target): void { foreach ($this->messages as $message) { + //@phpstan-ignore-next-line $target = $message->call($target) ?? $target; } }