From d13e0fa8585922b25deffff46f143c8dc1dd3d43 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 13 Aug 2026 18:00:44 +0200 Subject: [PATCH] Derive the type-check verdict in FuncCallHandler TypeSpecifyingFunctionsDynamicReturnTypeExtension existed only to pin array_key_exists()/key_exists()/in_array()/is_subclass_of() calls to ConstantBooleanType when the check is decided - and it needed the TypeSpecifierAwareExtension setter dance to construct ImpossibleCheckTypeHelper. The verdict now lives at the same decision point inside FuncCallHandler's dynamic-return-type resolution, with the helper constructor-injected; the extension is deleted. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7 --- src/Analyser/ExprHandler/FuncCallHandler.php | 16 ++++ ...ingFunctionsDynamicReturnTypeExtension.php | 77 ------------------- 2 files changed, 16 insertions(+), 77 deletions(-) delete mode 100644 src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 212d20038b1..689638715c3 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -48,6 +48,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\AccessoryArrayListType; use PHPStan\Type\Accessory\HasPropertyType; @@ -56,6 +57,7 @@ use PHPStan\Type\ClosureType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionThrowTypeExtension; use PHPStan\Type\DynamicReturnTypeExtensionRegistry; use PHPStan\Type\ErrorType; @@ -107,6 +109,7 @@ public function __construct( #[AutowiredParameter] private bool $rememberPossiblyImpureFunctionValues, private ExpressionResultFactory $expressionResultFactory, + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, ) { } @@ -1053,6 +1056,19 @@ private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $no } } + // for always-true/always-false type checks the call's own narrowing + // decides the return type - the verdict reads the same specified types + // the check contributes when used as a condition + if ( + $normalizedNode->name instanceof Name + && in_array($normalizedNode->name->toLowerString(), ['array_key_exists', 'key_exists', 'in_array', 'is_subclass_of'], true) + ) { + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $normalizedNode); + if ($isAlways !== null) { + return new ConstantBooleanType($isAlways); + } + } + return null; } diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php deleted file mode 100644 index fdc77b5e4a5..00000000000 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ /dev/null @@ -1,77 +0,0 @@ -typeSpecifier = $typeSpecifier; - } - - public function isFunctionSupported(FunctionReflection $functionReflection): bool - { - return in_array($functionReflection->getName(), [ - 'array_key_exists', - 'key_exists', - 'in_array', - 'is_subclass_of', - ], true); - } - - public function getTypeFromFunctionCall( - FunctionReflection $functionReflection, - FuncCall $functionCall, - Scope $scope, - ): ?Type - { - if (count($functionCall->getArgs()) === 0) { - return null; - } - - $isAlways = $this->getHelper()->findSpecifiedType( - $scope, - $functionCall, - ); - if ($isAlways === null) { - return null; - } - - return new ConstantBooleanType($isAlways); - } - - private function getHelper(): ImpossibleCheckTypeHelper - { - return $this->helper ??= new ImpossibleCheckTypeHelper($this->reflectionProvider, $this->typeSpecifier, $this->treatPhpDocTypesAsCertain); - } - -}