diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index c016b2869e..7d378afa71 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -331,21 +331,6 @@ public function specifyTypesInCondition( } } - if ( - !$context->null() - && $expr->right instanceof FuncCall - && count($expr->right->getArgs()) >= 3 - && $expr->right->name instanceof Name - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - return $this->specifyTypesInCondition( - $scope, - new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))), - $context, - )->setRootExpr($expr); - } - if ( !$context->null() && $expr->right instanceof FuncCall @@ -466,6 +451,24 @@ public function specifyTypesInCondition( } } + if ( + !$context->null() + && $expr->right instanceof Expr\CallLike + ) { + if (!$scope instanceof MutatingScope) { + throw new ShouldNotHappenException(); + } + $newScope = $scope->filterBySpecifiedTypes($result); + $callType = $newScope->getType($expr->right); + $newContext = $context->true() ? TypeSpecifierContext::createTrue($callType) : TypeSpecifierContext::createTrue($callType)->negate(); + + $result = $result->unionWith($this->specifyTypesInCondition( + $scope, + $expr->right, + $newContext, + )->setRootExpr($expr)); + } + return $result; } elseif ($expr instanceof Node\Expr\BinaryOp\Greater) { @@ -1173,7 +1176,7 @@ private function specifyTypesForConstantBinaryExpression( return $types->unionWith($this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createTrue() : TypeSpecifierContext::createTrue()->negate(), + $context->true() ? TypeSpecifierContext::createTrue($context->getReturnType()) : TypeSpecifierContext::createTrue($context->getReturnType())->negate(), )->setRootExpr($rootExpr)); } @@ -1973,7 +1976,7 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif return $this->specifyTypesInCondition( $scope, $exprNode, - $context->true() ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createFalsey()->negate(), + $context->true() ? TypeSpecifierContext::createFalsey($context->getReturnType()) : TypeSpecifierContext::createFalsey($context->getReturnType())->negate(), )->setRootExpr($expr); } diff --git a/src/Analyser/TypeSpecifierContext.php b/src/Analyser/TypeSpecifierContext.php index fe09aa861c..4044869c11 100644 --- a/src/Analyser/TypeSpecifierContext.php +++ b/src/Analyser/TypeSpecifierContext.php @@ -3,6 +3,9 @@ namespace PHPStan\Analyser; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @api @@ -21,34 +24,42 @@ final class TypeSpecifierContext /** @var self[] */ private static array $registry; - private function __construct(private ?int $value) + private function __construct( + private ?int $value, + private ?Type $returnType, + ) { } - private static function create(?int $value): self + private static function create(?int $value, ?Type $returnType = null): self { - self::$registry[$value] ??= new self($value); + if ($returnType !== null) { + // return type bound context is unique for each context and therefore not cacheable + return new self($value, $returnType); + } + + self::$registry[$value] ??= new self($value, null); return self::$registry[$value]; } - public static function createTrue(): self + public static function createTrue(?Type $returnType = null): self { - return self::create(self::CONTEXT_TRUE); + return self::create(self::CONTEXT_TRUE, $returnType); } - public static function createTruthy(): self + public static function createTruthy(?Type $returnType = null): self { - return self::create(self::CONTEXT_TRUTHY); + return self::create(self::CONTEXT_TRUTHY, $returnType); } - public static function createFalse(): self + public static function createFalse(?Type $returnType = null): self { - return self::create(self::CONTEXT_FALSE); + return self::create(self::CONTEXT_FALSE, $returnType); } - public static function createFalsey(): self + public static function createFalsey(?Type $returnType = null): self { - return self::create(self::CONTEXT_FALSEY); + return self::create(self::CONTEXT_FALSEY, $returnType); } public static function createNull(): self @@ -61,7 +72,14 @@ public function negate(): self if ($this->value === null) { throw new ShouldNotHappenException(); } - return self::create(~$this->value & self::CONTEXT_BITMASK); + + $negatedReturnType = null; + if ($this->returnType !== null) { + $baseType = $this->returnType->generalize(GeneralizePrecision::lessSpecific()); + $negatedReturnType = TypeCombinator::remove($baseType, $this->returnType); + } + + return self::create(~$this->value & self::CONTEXT_BITMASK, $negatedReturnType); } public function true(): bool @@ -89,4 +107,9 @@ public function null(): bool return $this->value === null; } + public function getReturnType(): ?Type + { + return $this->returnType; + } + } diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index 997cfea752..a232d25a2b 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -147,7 +147,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( $node->getArgs()[1]->value, new ArrayType(new MixedType(), $arrayValueType), - TypeSpecifierContext::createTrue(), + TypeSpecifierContext::createTrue($context->getReturnType()), $scope, )); } diff --git a/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeExtension.neon b/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeExtension.neon new file mode 100644 index 0000000000..6bb9f238df --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeExtension.neon @@ -0,0 +1,5 @@ +services: + - + class: PHPStan\Tests\TypeSpecifierContextReturnTypeExtension + tags: + - phpstan.typeSpecifier.methodTypeSpecifyingExtension diff --git a/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeTest.php b/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeTest.php new file mode 100644 index 0000000000..cf404559b5 --- /dev/null +++ b/tests/PHPStan/Analyser/TypeSpecifierContextReturnTypeTest.php @@ -0,0 +1,35 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Analyser; + +use PHPStan\Testing\TypeInferenceTestCase; + +class TypeSpecifierContextReturnTypeTest extends TypeInferenceTestCase +{ + + public function dataContextReturnType(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/type-specifier-context-return-type.php'); + } + + /** + * @dataProvider dataContextReturnType + * @param mixed ...$args + */ + public function testContextReturnType( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/TypeSpecifierContextReturnTypeExtension.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/data/TypeSpecifierContextReturnTypeExtension.php b/tests/PHPStan/Analyser/data/TypeSpecifierContextReturnTypeExtension.php new file mode 100644 index 0000000000..906bd0f304 --- /dev/null +++ b/tests/PHPStan/Analyser/data/TypeSpecifierContextReturnTypeExtension.php @@ -0,0 +1,59 @@ +<?php declare(strict_types = 1); + +namespace PHPStan\Tests; + +use PhpParser\Node\Expr\MethodCall; +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\MethodTypeSpecifyingExtension; +use TypeSpecifierContextReturnTypeTest\ContextReturnType; +use function str_starts_with; + +class TypeSpecifierContextReturnTypeExtension implements MethodTypeSpecifyingExtension, TypeSpecifierAwareExtension +{ + + private TypeSpecifier $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return ContextReturnType::class; + } + + public function isMethodSupported( + MethodReflection $methodReflection, + MethodCall $node, + TypeSpecifierContext $context, + ): bool + { + return str_starts_with($methodReflection->getName(), 'returns'); + } + + public function specifyTypes( + MethodReflection $methodReflection, + MethodCall $node, + Scope $scope, + TypeSpecifierContext $context, + ): SpecifiedTypes + { + if ($context->null()) { + return new SpecifiedTypes(); + } + + return $this->typeSpecifier->create( + $node->getArgs()[0]->value, + $context->getReturnType(), + $context, + $scope, + ); + } + +} diff --git a/tests/PHPStan/Analyser/data/type-specifier-context-return-type.php b/tests/PHPStan/Analyser/data/type-specifier-context-return-type.php new file mode 100644 index 0000000000..2303d938f6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/type-specifier-context-return-type.php @@ -0,0 +1,29 @@ +<?php + +namespace TypeSpecifierContextReturnTypeTest; + +use function PHPStan\Testing\assertType; + +class ContextReturnType { + public function returnsInt(int $specifiedContextReturnType): int {} + + public function doFooLeft(int $i) { + assertType('int', $i); + if ($this->returnsInt($i) > 0) { + assertType('int<1, max>', $i); + } else { + assertType('int<min, 0>', $i); + } + assertType('int', $i); + } + + public function doFooRight(int $i) { + assertType('int', $i); + if (0 < $this->returnsInt($i)) { + assertType('int<1, max>', $i); + } else { + assertType('int<min, 0>', $i); + } + assertType('int', $i); + } +}