diff --git a/src/Type/InspectorTypeExtension.php b/src/Type/InspectorTypeExtension.php index d090bc62..104c8882 100644 --- a/src/Type/InspectorTypeExtension.php +++ b/src/Type/InspectorTypeExtension.php @@ -31,8 +31,6 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\UnionType; use Stringable; -use function class_exists; -use function interface_exists; final class InspectorTypeExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { @@ -100,20 +98,20 @@ public function specifyTypes(MethodReflection $staticMethodReflection, StaticCal */ private function specifyAssertAll(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $callable = $node->getArgs()[0]->value; - $callableInfo = $scope->getType($callable); + $callableArg = $node->getArgs()[0]->value; + $callableType = $scope->getType($callableArg); - if (!$callableInfo->isCallable()->yes()) { + if (!$callableType->isCallable()->yes()) { return new SpecifiedTypes(); } - $traversable = $node->getArgs()[1]->value; - $traversableInfo = $scope->getType($traversable); + $traversableArg = $node->getArgs()[1]->value; + $traversableType = $scope->getType($traversableArg); // If it is already not mixed (narrowed by other code, like // '::assertAllArray()'), we could not provide any additional // information. We can only narrow this method to 'array'. - if (!$traversableInfo->equals(new MixedType())) { + if (!$traversableType->equals(new MixedType())) { return new SpecifiedTypes(); } @@ -130,13 +128,11 @@ private function specifyAssertAll(MethodReflection $staticMethodReflection, Stat return new SpecifiedTypes(); } - return $this->typeSpecifier->create( - $node->getArgs()[1]->value, - new IterableType(new MixedType(), new MixedType()), - $context, + $newType = new IterableType(new MixedType(), new MixedType()); + + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -144,9 +140,12 @@ private function specifyAssertAll(MethodReflection $staticMethodReflection, Stat */ private function specifyAssertAllStrings(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), new StringType()); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), new StringType()), + $traversableArg, + $newType, $context, false, $scope, @@ -158,17 +157,16 @@ private function specifyAssertAllStrings(MethodReflection $staticMethodReflectio */ private function specifyAssertAllStringable(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; // Drupal considers string as part of "stringable" as well. - $stringable = TypeCombinator::union(new ObjectType(Stringable::class), new StringType()); - $newType = new IterableType(new MixedType(), $stringable); + $stringableType = TypeCombinator::union(new ObjectType(Stringable::class), new StringType()); + $newType = new IterableType(new MixedType(), $stringableType); - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -176,16 +174,15 @@ private function specifyAssertAllStringable(MethodReflection $staticMethodReflec */ private function specifyAssertAllArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; $arrayType = new ArrayType(new MixedType(), new MixedType()); $newType = new IterableType(new MixedType(), $arrayType); - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -193,20 +190,20 @@ private function specifyAssertAllArrays(MethodReflection $staticMethodReflection */ private function specifyAssertStrictArray(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; $newType = new ArrayType( - // In Drupal, 'strict arrays' are defined as arrays whose indexes - // consist of integers that are equal to or greater than 0. + // In Drupal, 'strict arrays' are defined as arrays whose + // indexes consist of integers that are equal to or greater + // than 0. IntegerRangeType::createAllGreaterThanOrEqualTo(0), new MixedType(), ); - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -214,6 +211,7 @@ private function specifyAssertStrictArray(MethodReflection $staticMethodReflecti */ private function specifyAssertAllStrictArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; $newType = new IterableType( new MixedType(), new ArrayType( @@ -222,13 +220,11 @@ private function specifyAssertAllStrictArrays(MethodReflection $staticMethodRefl ), ); - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -278,9 +274,12 @@ private function specifyAssertAllHaveKey(MethodReflection $staticMethodReflectio */ private function specifyAssertAllIntegers(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), new IntegerType()); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), new IntegerType()), + $traversableArg, + $newType, $context, false, $scope, @@ -292,9 +291,12 @@ private function specifyAssertAllIntegers(MethodReflection $staticMethodReflecti */ private function specifyAssertAllFloat(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), new FloatType()); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), new FloatType()), + $traversableArg, + $newType, $context, false, $scope, @@ -306,9 +308,12 @@ private function specifyAssertAllFloat(MethodReflection $staticMethodReflection, */ private function specifyAssertAllCallable(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), new CallableType()); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), new CallableType()), + $traversableArg, + $newType, $context, false, $scope, @@ -320,7 +325,8 @@ private function specifyAssertAllCallable(MethodReflection $staticMethodReflecti */ private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { - $non_empty_types = [ + $traversableArg = $node->getArgs()[0]->value; + $nonEmptyTypes = [ new NonEmptyArrayType(), new ObjectType('object'), new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), @@ -329,15 +335,13 @@ private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflecti new FloatType(), new ResourceType(), ]; - $newType = new IterableType(new MixedType(), new UnionType($non_empty_types)); + $newType = new IterableType(new MixedType(), new UnionType($nonEmptyTypes)); - return $this->typeSpecifier->create( - $node->getArgs()[0]->value, + return $this->typeSpecifier->create($traversableArg, $newType, $context, false, - $scope, - ); + $scope); } /** @@ -345,9 +349,12 @@ private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflecti */ private function specifyAssertAllNumeric(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), new UnionType([new IntegerType(), new FloatType()])); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), new UnionType([new IntegerType(), new FloatType()])), + $traversableArg, + $newType, $context, false, $scope, @@ -359,9 +366,12 @@ private function specifyAssertAllNumeric(MethodReflection $staticMethodReflectio */ private function specifyAssertAllMatch(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[1]->value; + $newType = new IterableType(new MixedType(), new StringType()); + return $this->typeSpecifier->create( - $node->getArgs()[1]->value, - new IterableType(new MixedType(), new StringType()), + $traversableArg, + $newType, $context, false, $scope, @@ -373,11 +383,14 @@ private function specifyAssertAllMatch(MethodReflection $staticMethodReflection, */ private function specifyAssertAllRegularExpressionMatch(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes { + $traversableArg = $node->getArgs()[1]->value; + // Drupal treats any non-string input in traversable as invalid + // value, so it is possible to narrow type here. + $newType = new IterableType(new MixedType(), new StringType()); + return $this->typeSpecifier->create( - $node->getArgs()[1]->value, - // Drupal treats any non-string input in traversable as invalid - // value, so it is possible to narrow type here. - new IterableType(new MixedType(), new StringType()), + $traversableArg, + $newType, $context, false, $scope, @@ -398,20 +411,18 @@ private function specifyAssertAllObjects(MethodReflection $staticMethodReflectio $argType = $scope->getType($arg->value); foreach ($argType->getConstantStrings() as $stringType) { - $classString = $stringType->getValue(); - // PHPStan does not recognize a string argument like '\\Stringable' - // as a class string, so we need to explicitly check it. - if (!class_exists($classString) && !interface_exists($classString)) { - continue; + if ($stringType->isClassString()->yes()) { + $objectTypes[] = new ObjectType($stringType->getValue()); } - - $objectTypes[] = new ObjectType($classString); } } + $traversableArg = $node->getArgs()[0]->value; + $newType = new IterableType(new MixedType(), TypeCombinator::union(...$objectTypes)); + return $this->typeSpecifier->create( - $node->getArgs()[0]->value, - new IterableType(new MixedType(), TypeCombinator::union(...$objectTypes)), + $traversableArg, + $newType, $context, false, $scope, diff --git a/tests/src/Type/data/inspector.php b/tests/src/Type/data/inspector.php index 59cba054..52533b6f 100644 --- a/tests/src/Type/data/inspector.php +++ b/tests/src/Type/data/inspector.php @@ -3,155 +3,206 @@ use Drupal\Component\Assertion\Inspector; use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\node\Entity\Node; use function PHPStan\Testing\assertType; -function mixed_function(): mixed { - return NULL; -} - -// Inspector::assertAll() -$callable = fn (string $value): bool => $value === 'foo'; -$input = mixed_function(); -if (Inspector::assertAll($callable, $input)) { - assertType("iterable", $input); -} -else { - assertType('mixed', $input); -} - -$input = mixed_function(); -$callable = is_string(...); -if (Inspector::assertAll($callable, $input)) { - assertType('iterable', $input); -} -else { - assertType('mixed', $input); -} - - -// Inspector::assertAllStrings() -$input = mixed_function(); -if (Inspector::assertAllStrings($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllStringable() -$input = mixed_function(); -if (Inspector::assertAllStringable($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllArrays() -$input = mixed_function(); -if (Inspector::assertAllArrays($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertStrictArray() -$input = mixed_function(); -if (Inspector::assertStrictArray($input)) { - assertType('array, mixed>', $input); -} -else { - assertType('mixed~array, mixed>', $input); -} - -// Inspector::assertAllStrictArrays() -$input = mixed_function(); -if (Inspector::assertAllStrictArrays($input)) { - assertType('iterable, mixed>>', $input); -} -else { - assertType('mixed~iterable, mixed>>', $input); -} - -// Inspector::assertAllHaveKey() -$input = mixed_function(); -if (Inspector::assertAllHaveKey($input, 'foo', 'baz')) { - assertType("iterable", $input); -} -else { - assertType("mixed~iterable", $input); -} - -// Inspector::assertAllIntegers() -$input = mixed_function(); -if (Inspector::assertAllIntegers($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllFloat() -$input = mixed_function(); -if (Inspector::assertAllFloat($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllCallable() -$input = mixed_function(); -if (Inspector::assertAllCallable($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllNotEmpty() -$input = mixed_function(); -if (Inspector::assertAllNotEmpty($input)) { - assertType('iterable|int<1, max>|object|resource|non-empty-string|non-empty-array>', $input); -} -else { - assertType('mixed~iterable|int<1, max>|object|resource|non-empty-string|non-empty-array>', $input); -} - -// Inspector::assertAllNumeric() -$input = mixed_function(); -if (Inspector::assertAllNumeric($input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllMatch() -$pattern = 'foo'; -$input = mixed_function(); -if (Inspector::assertAllMatch($pattern, $input, false)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} - -// Inspector::assertAllRegularExpressionMatch() -$input = mixed_function(); -if (Inspector::assertAllRegularExpressionMatch($pattern, $input)) { - assertType('iterable', $input); -} -else { - assertType('mixed~iterable', $input); -} +/** + * @see Inspector::assertAll() + */ +function assertAll(mixed $mixed_arg, array $array_arg): void { + $callable = is_string(...); -// Inspector::assertAllObjects() -$input = mixed_function(); -if (Inspector::assertAllObjects($input, TranslatableMarkup::class, '\\Stringable', '\\Drupal\\jsonapi\\JsonApiResource\\ResourceIdentifier')) { - assertType('iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|\Stringable>', $input); -} -else { - assertType('mixed~iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|\Stringable>', $input); + Inspector::assertAll($callable, $mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed', $mixed_arg); + + Inspector::assertAll($callable, $array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllStrings() + */ +function assertAllStrings(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllStrings($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllStrings($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllStringable() + */ +function assertAllStringable(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllStringable($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllStringable($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllArrays() + */ +function assertAllArrays(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllArrays($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllArrays($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertStrictArray() + */ +function assertStrictArray(mixed $mixed_arg, array $array_arg): void { + Inspector::assertStrictArray($mixed_arg) + ? assertType('array, mixed>', $mixed_arg) + : assertType('mixed~array, mixed>', $mixed_arg); + + Inspector::assertStrictArray($array_arg) + ? assertType('array, mixed>', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllStrictArrays() + */ +function assertAllStrictArrays(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllStrictArrays($mixed_arg) + ? assertType('iterable, mixed>>', $mixed_arg) + : assertType('mixed~iterable, mixed>>', $mixed_arg); + + Inspector::assertAllStrictArrays($array_arg) + ? assertType('array, mixed>>', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllHaveKey() + */ +function assertAllHaveKey(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllHaveKey($mixed_arg, 'foo', 'baz') + ? assertType("iterable", $mixed_arg) + : assertType("mixed~iterable", $mixed_arg); + + Inspector::assertAllHaveKey($array_arg, 'foo', 'baz') + ? assertType("array", $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllIntegers() + */ +function assertAllIntegers(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllIntegers($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllIntegers($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllFloat() + */ +function assertAllFloat(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllFloat($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllFloat($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllCallable() + */ +function assertAllCallable(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllCallable($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllCallable($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllNotEmpty() + */ +function assertAllNotEmpty(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllNotEmpty($mixed_arg) + ? assertType('iterable|int<1, max>|object|resource|non-empty-string|non-empty-array>', $mixed_arg) + : assertType('mixed~iterable|int<1, max>|object|resource|non-empty-string|non-empty-array>', $mixed_arg); + + Inspector::assertAllNotEmpty($array_arg) + ? assertType('array|int<1, max>|object|resource|non-empty-string|non-empty-array>', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllNumeric() + */ +function assertAllNumeric(mixed $mixed_arg, array $array_arg): void { + Inspector::assertAllNumeric($mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllNumeric($array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllMatch() + */ +function assertAllMatch(mixed $mixed_arg, array $array_arg): void { + $pattern = 'foo'; + Inspector::assertAllMatch($pattern, $mixed_arg, false) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllMatch($pattern, $array_arg, false) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllRegularExpressionMatch() + */ +function assertAllRegularExpressionMatch(mixed $mixed_arg, array $array_arg): void { + $pattern = 'foo'; + Inspector::assertAllRegularExpressionMatch($pattern, $mixed_arg) + ? assertType('iterable', $mixed_arg) + : assertType('mixed~iterable', $mixed_arg); + + Inspector::assertAllRegularExpressionMatch($pattern, $array_arg) + ? assertType('array', $array_arg) + : assertType('array', $array_arg); +} + +/** + * @see Inspector::assertAllObjects() + */ +function assertAllObjects(mixed $mixed_arg, array $array_arg): void { + // Note: 'TranslatableMarkup' is not mentioned in the final union type, + // because it is a subtype of '\Stringable'. + Inspector::assertAllObjects($mixed_arg, Node::class, TranslatableMarkup::class, '\\Stringable', '\\Drupal\\jsonapi\\JsonApiResource\\ResourceIdentifier') + ? assertType('iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|Drupal\node\Entity\Node|\Stringable>', $mixed_arg) + : assertType('mixed~iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|Drupal\node\Entity\Node|\Stringable>', $mixed_arg); + + Inspector::assertAllObjects($array_arg, Node::class, TranslatableMarkup::class, '\\Stringable', '\\Drupal\\jsonapi\\JsonApiResource\\ResourceIdentifier') + ? assertType('array<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|Drupal\node\Entity\Node|\Stringable>', $array_arg) + : assertType('array', $array_arg); }