diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 335b8dcdd0..0aafa3956b 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -26,6 +26,7 @@ use PHPStan\Node\Printer\ExprPrinter; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\Assertions; +use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\ExtendedParametersAcceptor; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; @@ -570,6 +571,13 @@ public function specifyTypesInCondition( } } + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); + } elseif ($expr instanceof FuncCall && !($expr->name instanceof Name)) { + $specifiedTypes = $this->specifyTypesFromCallableCall($context, $expr, $scope); + if ($specifiedTypes !== null) { + return $specifiedTypes; + } + return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope); } elseif ($expr instanceof MethodCall && $expr->name instanceof Node\Identifier) { $methodCalledOnType = $scope->getType($expr->var); @@ -1764,6 +1772,38 @@ static function (Type $type, callable $traverse) use ($templateTypeMap, &$contai return $types; } + private function specifyTypesFromCallableCall(TypeSpecifierContext $context, FuncCall $call, Scope $scope): ?SpecifiedTypes + { + if (!$call->name instanceof Expr) { + return null; + } + + $calleeType = $scope->getType($call->name); + + $assertions = null; + $parametersAcceptor = null; + if ($calleeType->isCallable()->yes()) { + $variants = $calleeType->getCallableParametersAcceptors($scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs($scope, $call->getArgs(), $variants); + if ($parametersAcceptor instanceof CallableParametersAcceptor) { + $assertions = $parametersAcceptor->getAsserts(); + } + } + + if ($assertions === null || $assertions->getAll() === [] || $parametersAcceptor === null) { + return null; + } + + $asserts = $assertions->mapTypes(static fn (Type $type) => TemplateTypeHelper::resolveTemplateTypes( + $type, + $parametersAcceptor->getResolvedTemplateTypeMap(), + $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + TemplateTypeVariance::createInvariant(), + )); + + return $this->specifyTypesFromAsserts($context, $call, $asserts, $parametersAcceptor, $scope); + } + /** * @return array */ diff --git a/src/Reflection/Callables/CallableParametersAcceptor.php b/src/Reflection/Callables/CallableParametersAcceptor.php index 30c0254cd1..bcef9878ee 100644 --- a/src/Reflection/Callables/CallableParametersAcceptor.php +++ b/src/Reflection/Callables/CallableParametersAcceptor.php @@ -3,6 +3,7 @@ namespace PHPStan\Reflection\Callables; use PHPStan\Node\InvalidateExprNode; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\TrinaryLogic; @@ -57,4 +58,6 @@ public function getUsedVariables(): array; */ public function mustUseReturnValue(): TrinaryLogic; + public function getAsserts(): Assertions; + } diff --git a/src/Reflection/Callables/FunctionCallableVariant.php b/src/Reflection/Callables/FunctionCallableVariant.php index ca45e92e3c..6c48e4b010 100644 --- a/src/Reflection/Callables/FunctionCallableVariant.php +++ b/src/Reflection/Callables/FunctionCallableVariant.php @@ -2,6 +2,7 @@ namespace PHPStan\Reflection\Callables; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\ExtendedParameterReflection; use PHPStan\Reflection\ExtendedParametersAcceptor; @@ -173,4 +174,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->function->mustUseReturnValue(); } + public function getAsserts(): Assertions + { + return $this->function->getAsserts(); + } + } diff --git a/src/Reflection/ExtendedCallableFunctionVariant.php b/src/Reflection/ExtendedCallableFunctionVariant.php index 4cac48123d..389893394e 100644 --- a/src/Reflection/ExtendedCallableFunctionVariant.php +++ b/src/Reflection/ExtendedCallableFunctionVariant.php @@ -37,6 +37,7 @@ public function __construct( private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, + private ?Assertions $assertions = null, ) { parent::__construct( @@ -86,4 +87,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->mustUseReturnValue; } + public function getAsserts(): Assertions + { + return $this->assertions ?? Assertions::createEmpty(); + } + } diff --git a/src/Reflection/GenericParametersAcceptorResolver.php b/src/Reflection/GenericParametersAcceptorResolver.php index 27d8cfc11c..d9b75bf3e0 100644 --- a/src/Reflection/GenericParametersAcceptorResolver.php +++ b/src/Reflection/GenericParametersAcceptorResolver.php @@ -130,6 +130,7 @@ public static function resolve(array $argTypes, ParametersAcceptor $parametersAc $originalParametersAcceptor->getUsedVariables(), $originalParametersAcceptor->acceptsNamedArguments(), $originalParametersAcceptor->mustUseReturnValue(), + $originalParametersAcceptor->getAsserts(), ); } diff --git a/src/Reflection/InaccessibleMethod.php b/src/Reflection/InaccessibleMethod.php index f340cf3127..68fce995f8 100644 --- a/src/Reflection/InaccessibleMethod.php +++ b/src/Reflection/InaccessibleMethod.php @@ -93,4 +93,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 07848715f3..0891f487b7 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -958,6 +958,7 @@ public function createFirstClassCallable( } $parameters = $variant->getParameters(); + $assertions = $function !== null ? $function->getAsserts() : Assertions::createEmpty(); $closureTypes[] = new ClosureType( $parameters, $returnType, @@ -970,6 +971,7 @@ public function createFirstClassCallable( $impurePoints, acceptsNamedArguments: $acceptsNamedArguments, mustUseReturnValue: $mustUseReturnValue, + assertions: $assertions, ); } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 427c5bee51..b4c9b3a382 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -897,6 +897,7 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara $acceptor->getUsedVariables(), $acceptor->acceptsNamedArguments(), $acceptor->mustUseReturnValue(), + $acceptor->getAsserts(), ); } diff --git a/src/Reflection/ResolvedFunctionVariantWithCallable.php b/src/Reflection/ResolvedFunctionVariantWithCallable.php index 17574ec30e..6f816fa0ac 100644 --- a/src/Reflection/ResolvedFunctionVariantWithCallable.php +++ b/src/Reflection/ResolvedFunctionVariantWithCallable.php @@ -29,6 +29,7 @@ public function __construct( private array $usedVariables, private TrinaryLogic $acceptsNamedArguments, private TrinaryLogic $mustUseReturnValue, + private ?Assertions $assertions = null, ) { } @@ -118,4 +119,9 @@ public function mustUseReturnValue(): TrinaryLogic return $this->mustUseReturnValue; } + public function getAsserts(): Assertions + { + return $this->assertions ?? Assertions::createEmpty(); + } + } diff --git a/src/Reflection/TrivialParametersAcceptor.php b/src/Reflection/TrivialParametersAcceptor.php index 2863ff83cd..157368d4c0 100644 --- a/src/Reflection/TrivialParametersAcceptor.php +++ b/src/Reflection/TrivialParametersAcceptor.php @@ -103,4 +103,9 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + } diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 9d9cfb65e4..380c87982a 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -12,6 +12,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; @@ -398,6 +399,11 @@ public function mustUseReturnValue(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getAsserts(): Assertions + { + return Assertions::createEmpty(); + } + public function toNumber(): Type { return new ErrorType(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index dac3a2a07f..0a380dff93 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -13,6 +13,7 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\PhpDocParser\Printer\Printer; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; use PHPStan\Reflection\Callables\SimpleThrowPoint; @@ -84,6 +85,8 @@ class ClosureType implements TypeWithClassName, CallableParametersAcceptor private TrinaryLogic $mustUseReturnValue; + private Assertions $assertions; + /** * @api * @param list|null $parameters @@ -107,6 +110,7 @@ public function __construct( private array $usedVariables = [], ?TrinaryLogic $acceptsNamedArguments = null, ?TrinaryLogic $mustUseReturnValue = null, + ?Assertions $assertions = null, ) { if ($acceptsNamedArguments === null) { @@ -126,6 +130,12 @@ public function __construct( $this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty(); $this->callSiteVarianceMap = $callSiteVarianceMap ?? TemplateTypeVarianceMap::createEmpty(); $this->impurePoints = $impurePoints ?? [new SimpleImpurePoint('functionCall', 'call to an unknown Closure', false)]; + $this->assertions = $assertions ?? Assertions::createEmpty(); + } + + public function getAsserts(): Assertions + { + return $this->assertions; } /** @@ -664,6 +674,7 @@ public function traverse(callable $cb): Type $this->usedVariables, $this->acceptsNamedArguments, $this->mustUseReturnValue, + $this->assertions, ); } @@ -715,6 +726,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type $this->usedVariables, $this->acceptsNamedArguments, $this->mustUseReturnValue, + $this->assertions, ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14249.php b/tests/PHPStan/Analyser/nsrt/bug-14249.php new file mode 100644 index 0000000000..f3408725b6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14249.php @@ -0,0 +1,83 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug14249; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-assert-if-true positive-int $value + */ +function is_positive_int(mixed $value): bool { + return is_int($value) && $value > 0; +} + +function f(mixed $v): void { + $f1 = is_positive_int(...); + $f2 = 'Bug14249\is_positive_int'; + + if (is_positive_int($v)) { + assertType('int<1, max>', $v); + } + + if ($f1($v)) { + assertType('int<1, max>', $v); + } + + if ($f2($v)) { + assertType('int<1, max>', $v); + } +} + + +/** + * @template T of bool + * @param T $if + * @phpstan-assert (T is true ? true : false) $condition + */ +function assertIfTemplated(mixed $condition, bool $if) +{ +} + +function doTemplated(): void { + $f1 = assertIfTemplated(...); + $f2 = 'Bug14249\assertIfTemplated'; + + $v = getMixed(); + assertIfTemplated($v, true); + assertType('true', $v); + + $v = getMixed(); + $f1($v, true); + assertType('true', $v); + + $v = getMixed(); + $f2($v, true); + assertType('true', $v); + + $v = getMixed(); + assertIfTemplated($v, false); + assertType('false', $v); + + $v = getMixed(); + $f1($v, false); + assertType('false', $v); + + $v = getMixed(); + $f2($v, false); + assertType('false', $v); +} + +function getMixed(): mixed {} + +function maybeCallable() { + $f2 = 'Bug14249\assertIfTemplated'; + if (rand(0,1)) { + $f2 = 'notCallable'; + } + + $v = getMixed(); + $f2($v, false); + assertType('mixed', $v); +}