diff --git a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php index b4cdcd0..707db50 100644 --- a/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php +++ b/src/Type/WebMozartAssert/AssertTypeSpecifyingExtension.php @@ -4,23 +4,24 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr\BinaryOp\BooleanAnd; -use PhpParser\Node\Expr\BinaryOp\NotIdentical; use PhpParser\Node\Expr\StaticCall; -use PhpParser\Node\Scalar\String_; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\IterableType; use PHPStan\Type\MixedType; use PHPStan\Type\ObjectType; use PHPStan\Type\StaticMethodTypeSpecifyingExtension; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; @@ -28,21 +29,24 @@ class AssertTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { - private const ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING = [ - 'stringNotEmpty', - 'startsWithLetter', - 'unicodeLetters', - 'alpha', - 'digits', - 'alnum', - 'lower', - 'upper', - 'uuid', - 'ip', - 'ipv4', - 'ipv6', - 'email', - 'notWhitespaceOnly', + private const ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER = [ + 'stringNotEmpty' => 1, + 'contains' => 2, + 'startsWith' => 2, + 'startsWithLetter' => 1, + 'endsWith' => 2, + 'unicodeLetters' => 1, + 'alpha' => 1, + 'digits' => 1, + 'alnum' => 1, + 'lower' => 1, + 'upper' => 1, + 'uuid' => 1, + 'ip' => 1, + 'ipv4' => 1, + 'ipv6' => 1, + 'email' => 1, + 'notWhitespaceOnly' => 1, ]; /** @var \Closure[] */ @@ -67,10 +71,6 @@ public function isStaticMethodSupported( TypeSpecifierContext $context ): bool { - if (in_array($staticMethodReflection->getName(), self::ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING, true)) { - return true; - } - if (substr($staticMethodReflection->getName(), 0, 6) === 'allNot') { $methods = [ 'allNotInstanceOf' => 2, @@ -84,6 +84,13 @@ public function isStaticMethodSupported( $trimmedName = self::trimName($staticMethodReflection->getName()); $resolvers = self::getExpressionResolvers(); + if ( + array_key_exists($trimmedName, self::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER) + && count($node->getArgs()) >= self::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER[$trimmedName] + ) { + return true; + } + if (!array_key_exists($trimmedName, $resolvers)) { return false; } @@ -113,6 +120,8 @@ public function specifyTypes( TypeSpecifierContext $context ): SpecifiedTypes { + $trimmedName = self::trimName($staticMethodReflection->getName()); + if (substr($staticMethodReflection->getName(), 0, 6) === 'allNot') { return $this->handleAllNot( $staticMethodReflection->getName(), @@ -120,38 +129,129 @@ public function specifyTypes( $scope ); } - $expression = self::createExpression($scope, $staticMethodReflection->getName(), $node->getArgs()); - if ($expression === null) { - return new SpecifiedTypes([], []); + + if (array_key_exists($trimmedName, self::ASSERTIONS_SUPPORTED_VIA_CUSTOM_TYPE_SPECIFIER)) { + $specifiedTypes = $this->specifyTypesViaCustomTypeSpecifier( + $staticMethodReflection, + $node, + $scope + ); + } else { + $expression = self::createExpression($scope, $staticMethodReflection->getName(), $node->getArgs()); + if ($expression === null) { + return new SpecifiedTypes([], []); + } + + $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition( + $scope, + $expression, + TypeSpecifierContext::createTruthy() + ); } - $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition( - $scope, - $expression, - TypeSpecifierContext::createTruthy() - ); if (substr($staticMethodReflection->getName(), 0, 3) === 'all') { - if (count($specifiedTypes->getSureTypes()) > 0) { - $sureTypes = $specifiedTypes->getSureTypes(); - reset($sureTypes); - $exprString = key($sureTypes); - $sureType = $sureTypes[$exprString]; - return $this->arrayOrIterable( - $scope, - $sureType[0], - function () use ($sureType): Type { - return $sureType[1]; - } - ); - } - if (count($specifiedTypes->getSureNotTypes()) > 0) { - throw new \PHPStan\ShouldNotHappenException(); - } + return $this->arrayOrIterable( + $scope, + $node->getArgs()[0]->value, + function () use ($specifiedTypes): Type { + return $this->getResultingTypeFromSpecifiedTypes($specifiedTypes); + } + ); + } + + if (substr($staticMethodReflection->getName(), 0, 6) === 'nullOr') { + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + TypeCombinator::addNull($this->getResultingTypeFromSpecifiedTypes($specifiedTypes)), + TypeSpecifierContext::createTruthy(), + true + ); } return $specifiedTypes; } + private function getResultingTypeFromSpecifiedTypes(SpecifiedTypes $specifiedTypes): Type + { + if (count($specifiedTypes->getSureTypes()) > 0) { + $sureTypes = $specifiedTypes->getSureTypes(); + reset($sureTypes); + $exprString = key($sureTypes); + $sureType = $sureTypes[$exprString]; + + $sureNotTypes = $specifiedTypes->getSureNotTypes(); + + return array_key_exists($exprString, $sureNotTypes) + ? TypeCombinator::remove($sureType[1], $sureNotTypes[$exprString][1]) + : $sureType[1]; + } + + throw new \PHPStan\ShouldNotHappenException(); + } + + private function specifyTypesViaCustomTypeSpecifier( + MethodReflection $staticMethodReflection, + StaticCall $node, + Scope $scope + ): SpecifiedTypes + { + $trimmedName = self::trimName($staticMethodReflection->getName()); + + $expression = $node->getArgs()[0]->value; + $typeBefore = $scope->getType($expression); + + // Adds support for calling via all* + $typeBefore = $typeBefore->isIterable()->yes() ? $typeBefore->getIterableValueType() : $typeBefore; + + switch ($trimmedName) { + case 'startsWithLetter': + case 'digits': + case 'alnum': + case 'lower': + case 'upper': + case 'uuid': + case 'notWhitespaceOnly': + // Assertions narrowing down to non-empty-string if the input is a string + $type = (new StringType())->isSuperTypeOf($typeBefore)->yes() + ? TypeCombinator::intersect($typeBefore, new AccessoryNonEmptyStringType()) + : $typeBefore; + + return $this->typeSpecifier->create( + $expression, + $type, + TypeSpecifierContext::createTruthy() + ); + case 'stringNotEmpty': + case 'unicodeLetters': + case 'alpha': + case 'ip': + case 'ipv4': + case 'ipv6': + case 'email': + // Assertions always narrowing down to non-empty-string + return $this->typeSpecifier->create( + $expression, + new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]), + TypeSpecifierContext::createTruthy() + ); + case 'contains': + case 'startsWith': + case 'endsWith': + // Assertions narrowing down to non-empty-string if the input is a string and arg1 is a non-empty-string + $type = (new StringType())->isSuperTypeOf($typeBefore)->yes() && $scope->getType($node->getArgs()[1]->value)->isNonEmptyString()->yes() + ? TypeCombinator::intersect($typeBefore, new AccessoryNonEmptyStringType()) + : $typeBefore; + + return $this->typeSpecifier->create( + $expression, + $type, + TypeSpecifierContext::createTruthy() + ); + } + + throw new \PHPStan\ShouldNotHappenException(); + } + /** * @param Scope $scope * @param string $name @@ -165,29 +265,10 @@ private static function createExpression( ): ?\PhpParser\Node\Expr { $trimmedName = self::trimName($name); - - if (in_array($trimmedName, self::ASSERTIONS_RESULTING_IN_NON_EMPTY_STRING, true)) { - return self::createIsNonEmptyStringExpression($args); - } - $resolvers = self::getExpressionResolvers(); $resolver = $resolvers[$trimmedName]; - $expression = $resolver($scope, ...$args); - if ($expression === null) { - return null; - } - if (substr($name, 0, 6) === 'nullOr') { - $expression = new \PhpParser\Node\Expr\BinaryOp\BooleanOr( - $expression, - new \PhpParser\Node\Expr\BinaryOp\Identical( - $args[0]->value, - new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null')) - ) - ); - } - - return $expression; + return $resolver($scope, ...$args); } /** @@ -486,27 +567,6 @@ private static function getExpressionResolvers(): array ) ); }, - 'contains' => function (Scope $scope, Arg $value, Arg $subString): \PhpParser\Node\Expr { - if ($scope->getType($subString->value)->isNonEmptyString()->yes()) { - return self::createIsNonEmptyStringExpression([$value]); - } - - return self::createIsStringExpression([$value]); - }, - 'startsWith' => function (Scope $scope, Arg $value, Arg $prefix): \PhpParser\Node\Expr { - if ($scope->getType($prefix->value)->isNonEmptyString()->yes()) { - return self::createIsNonEmptyStringExpression([$value]); - } - - return self::createIsStringExpression([$value]); - }, - 'endsWith' => function (Scope $scope, Arg $value, Arg $suffix): \PhpParser\Node\Expr { - if ($scope->getType($suffix->value)->isNonEmptyString()->yes()) { - return self::createIsNonEmptyStringExpression([$value]); - } - - return self::createIsStringExpression([$value]); - }, 'length' => function (Scope $scope, Arg $value, Arg $length): \PhpParser\Node\Expr { return new BooleanAnd( new \PhpParser\Node\Expr\FuncCall( @@ -709,32 +769,4 @@ private function arrayOrIterable( ); } - /** - * @param \PhpParser\Node\Arg[] $args - */ - private static function createIsStringExpression(array $args): \PhpParser\Node\Expr - { - return new \PhpParser\Node\Expr\FuncCall( - new \PhpParser\Node\Name('is_string'), - [$args[0]] - ); - } - - /** - * @param \PhpParser\Node\Arg[] $args - */ - private static function createIsNonEmptyStringExpression(array $args): \PhpParser\Node\Expr - { - return new BooleanAnd( - new \PhpParser\Node\Expr\FuncCall( - new \PhpParser\Node\Name('is_string'), - [$args[0]] - ), - new NotIdentical( - $args[0]->value, - new String_('') - ) - ); - } - } diff --git a/tests/Type/WebMozartAssert/data/array.php b/tests/Type/WebMozartAssert/data/array.php index 18b10fa..8f80b18 100644 --- a/tests/Type/WebMozartAssert/data/array.php +++ b/tests/Type/WebMozartAssert/data/array.php @@ -25,13 +25,16 @@ public function keyExists(array $a): void \PHPStan\Testing\assertType('array{foo: string, bar?: string}', $a); } - public function validArrayKey($a, bool $b): void + public function validArrayKey($a, bool $b, $c): void { Assert::validArrayKey($a); \PHPStan\Testing\assertType('int|string', $a); Assert::validArrayKey($b); \PHPStan\Testing\assertType('*NEVER*', $b); + + Assert::nullOrValidArrayKey($c); + \PHPStan\Testing\assertType('int|string|null', $c); } /** @@ -94,10 +97,13 @@ public function countBetween(array $a, array $b, array $c, array $d): void \PHPStan\Testing\assertType('*NEVER*', $d); } - public function isList($a): void + public function isList($a, $b): void { Assert::isList($a); \PHPStan\Testing\assertType('array', $a); + + Assert::nullOrIsList($b); + \PHPStan\Testing\assertType('array|null', $b); } } diff --git a/tests/Type/WebMozartAssert/data/collection.php b/tests/Type/WebMozartAssert/data/collection.php index 2cc14ae..0c9c584 100644 --- a/tests/Type/WebMozartAssert/data/collection.php +++ b/tests/Type/WebMozartAssert/data/collection.php @@ -13,6 +13,12 @@ public function allString(array $a): void \PHPStan\Testing\assertType('array', $a); } + public function allStringNotEmpty(array $a): void + { + Assert::allStringNotEmpty($a); + \PHPStan\Testing\assertType('array', $a); + } + public function allInteger(array $a, iterable $b, iterable $c): void { Assert::allInteger($a); @@ -55,6 +61,12 @@ public function allNotSame(array $a): void \PHPStan\Testing\assertType('array{1, -2|2, -3|3}', $a); } + public function allEmail(array $a): void + { + Assert::allEmail($a); + \PHPStan\Testing\assertType('array', $a); + } + public function allSubclassOf(array $a, $b): void { Assert::allSubclassOf($a, self::class); diff --git a/tests/Type/WebMozartAssert/data/comparison.php b/tests/Type/WebMozartAssert/data/comparison.php index 7764b62..0efc1f7 100644 --- a/tests/Type/WebMozartAssert/data/comparison.php +++ b/tests/Type/WebMozartAssert/data/comparison.php @@ -6,19 +6,25 @@ class ComparisonTest { - public function true($a): void + public function true($a, $b): void { Assert::true($a); \PHPStan\Testing\assertType('true', $a); + + Assert::nullOrTrue($b); + \PHPStan\Testing\assertType('true|null', $b); } - public function false($a): void + public function false($a, $b): void { Assert::false($a); \PHPStan\Testing\assertType('false', $a); + + Assert::nullOrFalse($b); + \PHPStan\Testing\assertType('false|null', $b); } - public function notFalse(int $a): void + public function notFalse($a, $b): void { /** @var int|false $a */ Assert::notFalse($a); @@ -37,10 +43,13 @@ public function notNull(?int $a): void \PHPStan\Testing\assertType('int', $a); } - public function same($a): void + public function same($a, $b): void { Assert::same($a, 1); \PHPStan\Testing\assertType('1', $a); + + Assert::nullOrSame($b, 1); + \PHPStan\Testing\assertType('1|null', $b); } /** @@ -61,9 +70,12 @@ public function inArray($a, $b): void \PHPStan\Testing\assertType('\'bar\'|\'foo\'|null', $b); } - public function oneOf($a): void + public function oneOf($a, $b): void { Assert::oneOf($a, [1, 2]); \PHPStan\Testing\assertType('1|2', $a); + + Assert::nullOrOneOf($b, [1, 2]); + \PHPStan\Testing\assertType('1|2|null', $b); } } diff --git a/tests/Type/WebMozartAssert/data/object.php b/tests/Type/WebMozartAssert/data/object.php index 1719ae5..e2993b2 100644 --- a/tests/Type/WebMozartAssert/data/object.php +++ b/tests/Type/WebMozartAssert/data/object.php @@ -7,28 +7,40 @@ class ObjectTest { - public function classExists($a): void + public function classExists($a, $b): void { Assert::classExists($a); \PHPStan\Testing\assertType('class-string', $a); + + Assert::nullOrClassExists($b); + \PHPStan\Testing\assertType('class-string|null', $b); } - public function subclassOf($a): void + public function subclassOf($a, $b): void { Assert::subclassOf($a, self::class); \PHPStan\Testing\assertType('class-string|PHPStan\Type\WebMozartAssert\ObjectTest', $a); + + Assert::nullOrSubclassOf($b, self::class); + \PHPStan\Testing\assertType('class-string|PHPStan\Type\WebMozartAssert\ObjectTest|null', $b); } - public function interfaceExists($a): void + public function interfaceExists($a, $b): void { Assert::interfaceExists($a); \PHPStan\Testing\assertType('class-string', $a); + + Assert::nullOrInterfaceExists($b); + \PHPStan\Testing\assertType('class-string|null', $b); } - public function implementsInterface($a): void + public function implementsInterface($a, $b): void { Assert::implementsInterface($a, ObjectFoo::class); \PHPStan\Testing\assertType('PHPStan\Type\WebMozartAssert\ObjectFoo', $a); + + Assert::nullOrImplementsInterface($b, ObjectFoo::class); + \PHPStan\Testing\assertType('PHPStan\Type\WebMozartAssert\ObjectFoo|null', $b); } public function propertyExists(object $a): void diff --git a/tests/Type/WebMozartAssert/data/string.php b/tests/Type/WebMozartAssert/data/string.php index 832debb..02b357c 100644 --- a/tests/Type/WebMozartAssert/data/string.php +++ b/tests/Type/WebMozartAssert/data/string.php @@ -10,73 +10,97 @@ class TestStrings /** * @param non-empty-string $b */ - public function contains(string $a, string $b): void + public function contains(string $a, string $b, string $c, $d): void { Assert::contains($a, $a); \PHPStan\Testing\assertType('string', $a); Assert::contains($a, $b); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrContains($c, $b); + \PHPStan\Testing\assertType('non-empty-string|null', $c); + + Assert::contains($d, $b); + \PHPStan\Testing\assertType('mixed', $d); // not further narrowed down because the assertion expects a string } /** * @param non-empty-string $b */ - public function startsWith(string $a, string $b): void + public function startsWith(string $a, string $b, string $c): void { Assert::startsWith($a, $a); \PHPStan\Testing\assertType('string', $a); Assert::startsWith($a, $b); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrStartsWith($c, $b); + \PHPStan\Testing\assertType('non-empty-string|null', $c); } - public function startsWithLetter(string $a): void + public function startsWithLetter(string $a, string $b): void { Assert::startsWithLetter($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrStartsWithLetter($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } /** * @param non-empty-string $b */ - public function endsWith(string $a, string $b): void + public function endsWith(string $a, string $b, string $c): void { Assert::endsWith($a, $a); \PHPStan\Testing\assertType('string', $a); Assert::endsWith($a, $b); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrEndsWith($c, $b); + \PHPStan\Testing\assertType('non-empty-string|null', $c); } - public function length(string $a, string $b): void + public function length(string $a, string $b, string $c): void { Assert::length($a, 0); \PHPStan\Testing\assertType('\'\'', $a); Assert::length($b, 1); \PHPStan\Testing\assertType('non-empty-string', $b); + + Assert::nullOrLength($c, 1); + \PHPStan\Testing\assertType('non-empty-string|null', $c); } - public function minLength(string $a, string $b): void + public function minLength(string $a, string $b, string $c): void { Assert::minLength($a, 0); \PHPStan\Testing\assertType('string', $a); Assert::minLength($b, 1); \PHPStan\Testing\assertType('non-empty-string', $b); + + Assert::nullOrMinLength($c, 1); + \PHPStan\Testing\assertType('non-empty-string|null', $c); } - public function maxLength(string $a, string $b): void + public function maxLength(string $a, string $b, string $c): void { Assert::maxLength($a, 0); \PHPStan\Testing\assertType('\'\'', $a); Assert::maxLength($b, 1); \PHPStan\Testing\assertType('string', $b); + + Assert::nullOrMaxLength($c, 1); + \PHPStan\Testing\assertType('string|null', $c); } - public function lengthBetween(string $a, string $b, string $c, string $d): void + public function lengthBetween(string $a, string $b, string $c, string $d, string $e): void { Assert::lengthBetween($a, 0, 0); \PHPStan\Testing\assertType('\'\'', $a); @@ -89,18 +113,27 @@ public function lengthBetween(string $a, string $b, string $c, string $d): void Assert::lengthBetween($d, 1, 1); \PHPStan\Testing\assertType('non-empty-string', $d); + + Assert::nullOrLengthBetween($e, 1, 1); + \PHPStan\Testing\assertType('non-empty-string|null', $e); } - public function unicodeLetters($a): void + public function unicodeLetters($a, $b): void { Assert::unicodeLetters($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrUnicodeLetters($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function alpha($a): void + public function alpha($a, $b): void { Assert::alpha($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrAlpha($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } public function digits(string $a): void @@ -127,40 +160,61 @@ public function upper(string $a): void \PHPStan\Testing\assertType('non-empty-string', $a); } - public function uuid(string $a): void + public function uuid(string $a, string $b): void { Assert::uuid($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrUuid($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function ip($a): void + public function ip($a, $b): void { Assert::ip($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrIp($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function ipv4($a): void + public function ipv4($a, $b): void { Assert::ipv4($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrIpv4($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function ipv6($a): void + public function ipv6($a, $b): void { Assert::ipv6($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrIpv6($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function email($a): void + public function email($a, $b): void { Assert::email($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrEmail($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } - public function notWhitespaceOnly(string $a): void + public function notWhitespaceOnly(string $a, string $b, $c): void { Assert::notWhitespaceOnly($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrNotWhitespaceOnly($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); + + Assert::notWhitespaceOnly($c); + \PHPStan\Testing\assertType('mixed', $c); // not further narrowed down because the assertion expects a string } } diff --git a/tests/Type/WebMozartAssert/data/type.php b/tests/Type/WebMozartAssert/data/type.php index 5d71d7f..951fcd4 100644 --- a/tests/Type/WebMozartAssert/data/type.php +++ b/tests/Type/WebMozartAssert/data/type.php @@ -6,16 +6,22 @@ class TypeTest { - public function string($a): void + public function string($a, $b): void { Assert::string($a); \PHPStan\Testing\assertType('string', $a); + + Assert::nullOrString($b); + \PHPStan\Testing\assertType('string|null', $b); } - public function stringNotEmpty($a): void + public function stringNotEmpty($a, $b): void { Assert::stringNotEmpty($a); \PHPStan\Testing\assertType('non-empty-string', $a); + + Assert::nullOrStringNotEmpty($b); + \PHPStan\Testing\assertType('non-empty-string|null', $b); } public function integer($a, $b): void @@ -27,96 +33,138 @@ public function integer($a, $b): void \PHPStan\Testing\assertType('int|null', $b); } - public function integerish($a): void + public function integerish($a, $b): void { Assert::integerish($a); \PHPStan\Testing\assertType('float|int|numeric-string', $a); + + Assert::nullOrIntegerish($b); + \PHPStan\Testing\assertType('float|int|numeric-string|null', $b); } - public function positiveInteger($a): void + public function positiveInteger($a, $b, $c): void { Assert::positiveInteger($a); \PHPStan\Testing\assertType('int<1, max>', $a); - $b = -1; + /** @var -1 $b */ Assert::positiveInteger($b); \PHPStan\Testing\assertType('*NEVER*', $b); + + Assert::nullOrPositiveInteger($c); + \PHPStan\Testing\assertType('int<1, max>|null', $c); } - public function float($a): void + public function float($a, $b): void { Assert::float($a); \PHPStan\Testing\assertType('float', $a); + + Assert::nullOrFloat($b); + \PHPStan\Testing\assertType('float|null', $b); } - public function numeric($a): void + public function numeric($a, $b): void { Assert::numeric($a); \PHPStan\Testing\assertType('float|int|numeric-string', $a); + + Assert::nullOrNumeric($b); + \PHPStan\Testing\assertType('float|int|numeric-string|null', $b); } - public function natural($a): void + public function natural($a, $b, $c): void { Assert::natural($a); \PHPStan\Testing\assertType('int<0, max>', $a); - $b = -1; + /** @var -1 $b */ Assert::natural($b); \PHPStan\Testing\assertType('*NEVER*', $b); + + Assert::nullOrNatural($c); + \PHPStan\Testing\assertType('int<0, max>|null', $c); } - public function boolean($a): void + public function boolean($a, $b): void { Assert::boolean($a); \PHPStan\Testing\assertType('bool', $a); + + Assert::nullOrBoolean($b); + \PHPStan\Testing\assertType('bool|null', $b); } - public function scalar($a): void + public function scalar($a, $b): void { Assert::scalar($a); \PHPStan\Testing\assertType('bool|float|int|string', $a); + + Assert::nullOrScalar($b); + \PHPStan\Testing\assertType('bool|float|int|string|null', $b); } - public function object($a): void + public function object($a, $b): void { Assert::object($a); \PHPStan\Testing\assertType('object', $a); + + Assert::nullOrObject($b); + \PHPStan\Testing\assertType('object|null', $b); } - public function resource($a): void + public function resource($a, $b): void { Assert::resource($a); \PHPStan\Testing\assertType('resource', $a); + + Assert::nullOrResource($b); + \PHPStan\Testing\assertType('resource|null', $b); } - public function isCallable($a): void + public function isCallable($a, $b): void { Assert::isCallable($a); \PHPStan\Testing\assertType('callable(): mixed', $a); + + Assert::nullOrIsCallable($b); + \PHPStan\Testing\assertType('(callable(): mixed)|null', $b); } - public function isArray($a): void + public function isArray($a, $b): void { Assert::isArray($a); \PHPStan\Testing\assertType('array', $a); + + Assert::nullOrIsArray($b); + \PHPStan\Testing\assertType('array|null', $b); } - public function isIterable($a): void + public function isIterable($a, $b): void { Assert::isIterable($a); \PHPStan\Testing\assertType('array|Traversable', $a); + + Assert::nullOrIsIterable($b); + \PHPStan\Testing\assertType('array|Traversable|null', $b); } - public function isCountable($a): void + public function isCountable($a, $b): void { Assert::isCountable($a); \PHPStan\Testing\assertType('array|Countable', $a); + + Assert::nullOrIsCountable($b); + \PHPStan\Testing\assertType('array|Countable|null', $b); } - public function isInstanceOf($a): void + public function isInstanceOf($a, $b): void { Assert::isInstanceOf($a, self::class); \PHPStan\Testing\assertType('PHPStan\Type\WebMozartAssert\TypeTest', $a); + + Assert::nullOrIsInstanceOf($b, self::class); + \PHPStan\Testing\assertType('PHPStan\Type\WebMozartAssert\TypeTest|null', $b); } /** @@ -128,9 +176,12 @@ public function notInstanceOf($a): void \PHPStan\Testing\assertType('PHPStan\Type\WebMozartAssert\Foo', $a); } - public function isArrayAccessible($a): void + public function isArrayAccessible($a, $b): void { Assert::isArrayAccessible($a); \PHPStan\Testing\assertType('array|ArrayAccess', $a); + + Assert::nullOrIsArrayAccessible($b); + \PHPStan\Testing\assertType('array|ArrayAccess|null', $b); } } diff --git a/tests/WebMozartIntegration/PHPStanIntegrationTest.php b/tests/WebMozartIntegration/PHPStanIntegrationTest.php new file mode 100644 index 0000000..b0e37bf --- /dev/null +++ b/tests/WebMozartIntegration/PHPStanIntegrationTest.php @@ -0,0 +1,33 @@ +