From dfc3f29ac3a79b847532a23513f5adf1d8f4cb62 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 4 Nov 2026 12:45:15 +0000 Subject: [PATCH 1/2] Resolve boolean calls with `@phpstan-assert-if-true`/`-if-false` to `true`/`false` when the argument types already decide the assertion - ImpossibleCheckTypeHelper: add hasConditionalAsserts() and resolveConditionalAssertReturnType(), which pins a non-constant boolean return type to ConstantBooleanType using the existing findSpecifiedType() verdict, i.e. the verdict the ImpossibleCheckType* rules already report as "will always evaluate to true/false" - FuncCallHandler, MethodCallHandler, StaticCallHandler: when the callee has conditional asserts, the type callback applies that verdict after resolveReturnType(). The call's own ExpressionResult is read through a weak storage reference, as FuncCallHandler already does for in_array() etc. Nullsafe method calls are covered through MethodCallHandler. - Built-in checks like is_float() already resolved this way through their conditional return types. User-defined asserted checks stayed `bool`, so a `match (true)` whose last arm was such a call was reported as not handling `true`, and an if/elseif chain over them got "return statement is missing". - turbo-ext: mirror the change in FuncCallHandler.cpp, MethodCallHandler.cpp and StaticCallHandler.cpp (new constructor dependency, new type-callback captures). The WeakReference helpers move from FuncCallHandler.cpp into CallHandlerSupport.h so the three handlers share them. The generated declarations for the two method handlers are regenerated. - Probed and left alone: asserts on `$this->property` and first-class callables `$f = $obj->isX(...)` still give `bool`, consistently with the ImpossibleCheckType* rules, which do not decide these either. Unconditional `@phpstan-assert` and equality (`=Type`) asserts are unaffected. --- src/Analyser/ExprHandler/FuncCallHandler.php | 18 +- .../ExprHandler/MethodCallHandler.php | 41 +++- .../ExprHandler/StaticCallHandler.php | 41 +++- .../Comparison/ImpossibleCheckTypeHelper.php | 33 +++ tests/PHPStan/Analyser/nsrt/bug-11374.php | 78 ++++++++ .../Comparison/MatchExpressionRuleTest.php | 46 +++++ .../Rules/Comparison/data/bug-11374-2.php | 88 ++++++++ .../Rules/Comparison/data/bug-11374.php | 56 ++++++ .../Rules/Missing/MissingReturnRuleTest.php | 12 ++ .../PHPStan/Rules/Missing/data/bug-11374.php | 80 ++++++++ turbo-ext/src/CallHandlerSupport.h | 73 +++++++ turbo-ext/src/FuncCallHandler.cpp | 64 +++--- turbo-ext/src/MethodCallHandler.cpp | 43 ++-- turbo-ext/src/StaticCallHandler.cpp | 43 ++-- turbo-ext/src/generated/MethodCallHandler.h | 169 ++++++++-------- turbo-ext/src/generated/StaticCallHandler.h | 189 +++++++++--------- 16 files changed, 814 insertions(+), 260 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11374.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-11374-2.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-11374.php create mode 100644 tests/PHPStan/Rules/Missing/data/bug-11374.php diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 4ff33f80889..92f07c66a7a 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -406,9 +406,10 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // inside resolveReturnType), the acceptor is re-derived from the // already-processed argument results on the asking scope. $storageRef = WeakReference::create($storage); + $hasConditionalAsserts = $functionReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($functionReflection->getAsserts()); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) - : function (bool $nativeTypesPromoted) use ($nodeScopeResolver, $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef): Type { + : function (bool $nativeTypesPromoted) use ($nodeScopeResolver, $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { // for always-true/always-false type checks the call's own narrowing // (already produced as this result's specifyTypesCallback) decides // the return type - the verdict is a read of that narrowing, not a @@ -430,7 +431,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } - return $this->resolveReturnType( + $returnType = $this->resolveReturnType( $nodeScopeResolver, $beforeScope, $nativeTypesPromoted, @@ -439,6 +440,19 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $nativeTypesPromoted ? null : $resolvedParametersAcceptor, $argsResult, ); + if ($nativeTypesPromoted || !$hasConditionalAsserts) { + return $returnType; + } + + $callStorage = $storageRef->get(); + + return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType( + $beforeScope, + $expr, + $callStorage === null ? null : $callStorage->findExpressionResult($expr), + $argsResult, + $returnType, + ); }; $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 79c730ef67e..cc7f0b69616 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -43,6 +43,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ErrorType; @@ -55,6 +56,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; +use WeakReference; use function array_map; use function array_merge; use function count; @@ -81,6 +83,7 @@ public function __construct( private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, private ArgumentsHandler $argumentsHandler, + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, ) { } @@ -216,17 +219,37 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // evolving scope (type-driven, generics resolved). When null // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. + $storageRef = WeakReference::create($storage); + $hasConditionalAsserts = $methodReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($methodReflection->getAsserts()); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) - : fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType( - $beforeScope, - $nativeTypesPromoted, - $expr, - $varResult, - $nameResult, - $nativeTypesPromoted ? null : $resolvedParametersAcceptor, - $argsResult, - ); + : function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { + $returnType = $this->resolveReturnType( + $beforeScope, + $nativeTypesPromoted, + $expr, + $varResult, + $nameResult, + $nativeTypesPromoted ? null : $resolvedParametersAcceptor, + $argsResult, + ); + if ($nativeTypesPromoted || !$hasConditionalAsserts) { + return $returnType; + } + + // the call's own narrowing decides a conditional @phpstan-assert; + // its result is looked up through a weak storage reference, as + // this callback is owned by that very result + $callStorage = $storageRef->get(); + + return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType( + $beforeScope, + $expr, + $callStorage === null ? null : $callStorage->findExpressionResult($expr), + $argsResult, + $returnType, + ); + }; $walkMethodReflection = $methodReflection; $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index a90ba1d98b2..c9733a54347 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -42,6 +42,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\ErrorType; @@ -56,6 +57,7 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeWithClassName; use ReflectionProperty; +use WeakReference; use function array_map; use function array_merge; use function count; @@ -82,6 +84,7 @@ public function __construct( private DynamicReturnTypeStoragePrimer $storagePrimer, private EarlyTerminatingCallHelper $earlyTerminatingHelper, private ArgumentsHandler $argumentsHandler, + private ImpossibleCheckTypeHelper $impossibleCheckTypeHelper, ) { } @@ -299,17 +302,37 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // evolving scope (type-driven, generics resolved). When null // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. + $storageRef = WeakReference::create($storage); + $hasConditionalAsserts = $methodReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($methodReflection->getAsserts()); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) - : fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType( - $beforeScope, - $nativeTypesPromoted, - $expr, - $classResult, - $nameResult, - $nativeTypesPromoted ? null : $resolvedParametersAcceptor, - $argsResult, - ); + : function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { + $returnType = $this->resolveReturnType( + $beforeScope, + $nativeTypesPromoted, + $expr, + $classResult, + $nameResult, + $nativeTypesPromoted ? null : $resolvedParametersAcceptor, + $argsResult, + ); + if ($nativeTypesPromoted || !$hasConditionalAsserts) { + return $returnType; + } + + // the call's own narrowing decides a conditional @phpstan-assert; + // its result is looked up through a weak storage reference, as + // this callback is owned by that very result + $callStorage = $storageRef->get(); + + return $this->impossibleCheckTypeHelper->resolveConditionalAssertReturnType( + $beforeScope, + $expr, + $callStorage === null ? null : $callStorage->findExpressionResult($expr), + $argsResult, + $returnType, + ); + }; $walkMethodReflection = $methodReflection; $specifyTypesCallback = fn (TypeSpecifierContext $specifyContext, bool $nativeTypesPromoted): SpecifiedTypes => $this->specifyTypes( $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope, diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 49af429e6de..2e278595f7e 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -15,6 +15,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; @@ -93,6 +94,38 @@ public function findSpecifiedType( return $specifiedValue; } + public static function hasConditionalAsserts(Assertions $assertions): bool + { + $all = $assertions->getAll(); + + return $all !== [] && count($all) > count($assertions->getAsserts()); + } + + /** + * Pins a boolean call with @phpstan-assert-if-true/-if-false to true/false when + * the argument types already decide the assertion - the same verdict the + * ImpossibleCheckType* rules report, read from the call's own narrowing. + */ + public function resolveConditionalAssertReturnType( + Scope $scope, + Expr $node, + ?ExpressionResult $nodeResult, + ?ArgsResult $argsResult, + Type $returnType, + ): Type + { + if ($nodeResult === null || !$returnType->isBoolean()->yes() || $returnType->isConstantScalarValue()->yes()) { + return $returnType; + } + + $isAlways = $this->findSpecifiedType($scope, $node, $nodeResult, $argsResult); + if ($isAlways === null) { + return $returnType; + } + + return new ConstantBooleanType($isAlways); + } + /** * Reads an argument's type from its already-computed ExpressionResult when the * caller passed the call's ArgsResult (the engine-side verdict in diff --git a/tests/PHPStan/Analyser/nsrt/bug-11374.php b/tests/PHPStan/Analyser/nsrt/bug-11374.php new file mode 100644 index 00000000000..ff87f4a9932 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11374.php @@ -0,0 +1,78 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11374Types; + +use function PHPStan\Testing\assertType; + +final class Checker +{ + /** @phpstan-assert-if-true float $value */ + public function isFloat(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert-if-false float $value */ + public function isNotFloat(mixed $value): bool + { + return !is_float($value); + } + + /** @phpstan-assert-if-true !float $value */ + public function isNotFloatNegated(mixed $value): bool + { + return !is_float($value); + } + + /** @phpstan-assert-if-true float $value */ + public static function isFloatStatic(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert float $value */ + public function assertFloat(mixed $value): bool + { + return true; + } + + /** @phpstan-assert-if-true =float $value */ + public function isFloatEquality(mixed $value): bool + { + return is_float($value); + } +} + +/** @phpstan-assert-if-true float $value */ +function isFloat(mixed $value): bool +{ + return is_float($value); +} + +function (Checker $c, float $f, int $i, float|int $fi, mixed $m): void { + assertType('true', $c->isFloat($f)); + assertType('false', $c->isFloat($i)); + assertType('bool', $c->isFloat($fi)); + assertType('bool', $c->isFloat($m)); + + assertType('false', $c->isNotFloat($f)); + assertType('true', $c->isNotFloat($i)); + assertType('bool', $c->isNotFloat($fi)); + + assertType('false', $c->isNotFloatNegated($f)); + assertType('true', $c->isNotFloatNegated($i)); + assertType('bool', $c->isNotFloatNegated($fi)); + + assertType('true', Checker::isFloatStatic($f)); + assertType('false', Checker::isFloatStatic($i)); + assertType('bool', Checker::isFloatStatic($fi)); + + assertType('true', isFloat($f)); + assertType('false', isFloat($i)); + assertType('bool', isFloat($fi)); + + assertType('bool', $c->assertFloat($i)); + assertType('bool', $c->isFloatEquality($f)); +}; diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 600a95e868d..aea752be44e 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -534,6 +534,52 @@ public function testNullInMultiConditionArm(): void $this->analyse([__DIR__ . '/data/match-null-in-multi-condition-arm.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug11374(): void + { + $this->analyse([__DIR__ . '/data/bug-11374.php'], [ + [ + 'Call to function is_float() with float will always evaluate to true.', + 13, + ], + [ + 'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.', + 21, + ], + [ + 'Call to method Bug11374\\HelloWorld::isFloat() with float will always evaluate to true.', + 29, + ], + [ + 'Call to function is_float() with float will always evaluate to true.', + 37, + ], + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug11374Analogues(): void + { + $this->analyse([__DIR__ . '/data/bug-11374-2.php'], [ + [ + 'Call to function isFloat() with float will always evaluate to true.', + 13, + ], + [ + 'Call to static method Bug11374Two\\HelloWorld::isFloatStatic() with float will always evaluate to true.', + 21, + ], + [ + 'Call to method Bug11374Two\\HelloWorld::isNotFloat() with float will always evaluate to false.', + 29, + ], + [ + 'Match expression does not handle remaining value: true', + 35, + ], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testBug11453(): void { diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11374-2.php b/tests/PHPStan/Rules/Comparison/data/bug-11374-2.php new file mode 100644 index 00000000000..6b0ff617c39 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11374-2.php @@ -0,0 +1,88 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11374Two; + +final class HelloWorld +{ + public function functions(float|int $value): string + { + return match (true) { + isInt($value) => (string) $value, + isFloat($value) => (string) $value, + }; + } + + public function staticMethods(float|int $value): string + { + return match (true) { + self::isIntStatic($value) => (string) $value, + self::isFloatStatic($value) => (string) $value, + }; + } + + public function assertIfFalse(float|int $value): string + { + return match (false) { + $this->isNotInt($value) => (string) $value, + $this->isNotFloat($value) => (string) $value, + }; + } + + public function stillUnhandled(float|int|string $value): string + { + return match (true) { + $this->isInt($value) => (string) $value, + $this->isFloat($value) => (string) $value, + }; + } + + /** @phpstan-assert-if-true float $value */ + private function isFloat(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert-if-true int $value */ + private function isInt(mixed $value): bool + { + return is_int($value); + } + + /** @phpstan-assert-if-false float $value */ + private function isNotFloat(mixed $value): bool + { + return !is_float($value); + } + + /** @phpstan-assert-if-false int $value */ + private function isNotInt(mixed $value): bool + { + return !is_int($value); + } + + /** @phpstan-assert-if-true float $value */ + private static function isFloatStatic(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert-if-true int $value */ + private static function isIntStatic(mixed $value): bool + { + return is_int($value); + } +} + +/** @phpstan-assert-if-true float $value */ +function isFloat(mixed $value): bool +{ + return is_float($value); +} + +/** @phpstan-assert-if-true int $value */ +function isInt(mixed $value): bool +{ + return is_int($value); +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-11374.php b/tests/PHPStan/Rules/Comparison/data/bug-11374.php new file mode 100644 index 00000000000..f767ebf8b15 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-11374.php @@ -0,0 +1,56 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11374; + +class HelloWorld +{ + public function test(float|int $value): string + { + return match (true) { + is_int($value) => (string) $value, + is_float($value) => (string) $value, + }; + } + + public function test2(float|int $value): string + { + return match (true) { + $this->isInt($value) => (string) $value, + $this->isFloat($value) => (string) $value, + }; + } + + public function test3(float|int $value): string + { + return match (true) { + is_int($value) => (string) $value, + $this->isFloat($value) => (string) $value, + }; + } + + public function test4(float|int $value): string + { + return match (true) { + $this->isInt($value) => (string) $value, + is_float($value) => (string) $value, + }; + } + + /** + * @phpstan-assert-if-true float $value + */ + private function isFloat(mixed $value): bool + { + return is_float($value); + } + + /** + * @phpstan-assert-if-true int $value + */ + private function isInt(mixed $value): bool + { + return is_int($value); + } +} diff --git a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php index ab9ef5d5f8e..d74a7d47681 100644 --- a/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php +++ b/tests/PHPStan/Rules/Missing/MissingReturnRuleTest.php @@ -380,6 +380,18 @@ public function testBug14638(): void $this->analyse([__DIR__ . '/data/bug-14638.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug11374(): void + { + $this->checkExplicitMixedMissingReturn = true; + $this->analyse([__DIR__ . '/data/bug-11374.php'], [ + [ + 'Method Bug11374Missing\\HelloWorld::stillMissing() should return string but return statement is missing.', + 38, + ], + ]); + } + public function testBug5681(): void { $this->checkExplicitMixedMissingReturn = true; diff --git a/tests/PHPStan/Rules/Missing/data/bug-11374.php b/tests/PHPStan/Rules/Missing/data/bug-11374.php new file mode 100644 index 00000000000..cad91796883 --- /dev/null +++ b/tests/PHPStan/Rules/Missing/data/bug-11374.php @@ -0,0 +1,80 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11374Missing; + +final class HelloWorld +{ + public function methods(float|int $value): string + { + if ($this->isInt($value)) { + return 'int'; + } elseif ($this->isFloat($value)) { + return 'float'; + } + } + + public function staticMethods(float|int $value): string + { + if (self::isIntStatic($value)) { + return 'int'; + } elseif (self::isFloatStatic($value)) { + return 'float'; + } + } + + public function functions(float|int $value): string + { + if (isInt($value)) { + return 'int'; + } elseif (isFloat($value)) { + return 'float'; + } + } + + public function stillMissing(float|int|string $value): string + { + if ($this->isInt($value)) { + return 'int'; + } elseif ($this->isFloat($value)) { + return 'float'; + } + } + + /** @phpstan-assert-if-true float $value */ + private function isFloat(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert-if-true int $value */ + private function isInt(mixed $value): bool + { + return is_int($value); + } + + /** @phpstan-assert-if-true float $value */ + private static function isFloatStatic(mixed $value): bool + { + return is_float($value); + } + + /** @phpstan-assert-if-true int $value */ + private static function isIntStatic(mixed $value): bool + { + return is_int($value); + } +} + +/** @phpstan-assert-if-true float $value */ +function isFloat(mixed $value): bool +{ + return is_float($value); +} + +/** @phpstan-assert-if-true int $value */ +function isInt(mixed $value): bool +{ + return is_int($value); +} diff --git a/turbo-ext/src/CallHandlerSupport.h b/turbo-ext/src/CallHandlerSupport.h index 8a7f6634d2e..3ef3632cdf6 100644 --- a/turbo-ext/src/CallHandlerSupport.h +++ b/turbo-ext/src/CallHandlerSupport.h @@ -23,6 +23,7 @@ #include "AcceptorValues.h" #include "zend_smart_str.h" +#include "zend_weakrefs.h" namespace ptcall { @@ -133,6 +134,34 @@ inline zv::Val assertionsGetAll(zval *assertions) return hold.isUndef() ? zv::Val::copyOf(zv::Ref(all)) : std::move(hold); } +/* ImpossibleCheckTypeHelper::hasConditionalAsserts($assertions): whether any + * tag is @phpstan-assert-if-true/-if-false; false = pending exception */ +[[nodiscard]] inline bool hasConditionalAsserts(zval *assertions, bool &out) +{ + zv::Val all = assertionsGetAll(assertions); + if (UNEXPECTED(all.isUndef())) return false; + if (Z_TYPE_P(all.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(all.raw())) == 0) { + out = false; + return true; + } + zv::Val unconditional = pt_assertions_get_asserts(assertions, PT_ASSERTIONS_UNCONDITIONAL); + if (UNEXPECTED(unconditional.isUndef())) return false; + out = Z_TYPE_P(unconditional.raw()) == IS_ARRAY + && zend_hash_num_elements(Z_ARRVAL_P(all.raw())) > zend_hash_num_elements(Z_ARRVAL_P(unconditional.raw())); + return true; +} + +inline pt_method_site resolveConditionalAssertReturnTypeSite; + +/* $impossibleCheckTypeHelper->resolveConditionalAssertReturnType($scope, $node, + * $nodeResult, $argsResult, $returnType) — $nodeResult / $argsResult may be + * PHP null */ +inline zv::Val resolveConditionalAssertReturnType(zval *impossibleCheckTypeHelper, zval *scope, zval *node, zval *nodeResult, zval *argsResult, zval *returnType) +{ + zv::Args argv{scope, node, nodeResult, argsResult, returnType}; + return pt_call_method_cached(resolveConditionalAssertReturnTypeSite, Z_OBJ_P(impossibleCheckTypeHelper), PT_LC("resolveconditionalassertreturntype"), 5, argv); +} + /* $assertions->mapTypes($callable) */ inline zv::Val assertionsMapTypes(zval *assertions, zval *callable) { @@ -281,6 +310,50 @@ inline void resolveAssertType(zval *captures, uint32_t argc, zval *argv, zval *r type.intoReturnValue(return_value); } +/* WeakReference::create($referent) / $weakReference->get() */ +inline zend_function *weakReferenceCreateFn = NULL; +inline zend_function *weakReferenceGetFn = NULL; + +inline zv::Val weakReferenceCreate(zval *referent) +{ + if (UNEXPECTED(weakReferenceCreateFn == NULL)) { + weakReferenceCreateFn = pt_find_method(zend_ce_weakref, PT_LC("create")); + if (UNEXPECTED(weakReferenceCreateFn == NULL)) return zv::Val(); + } + zval ret; + zend_call_known_function(weakReferenceCreateFn, NULL, zend_ce_weakref, &ret, 1, referent, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&ret); + return zv::Val(); + } + return zv::Val::adopt(ret); +} + +inline zv::Val weakReferenceGet(zval *weakReference) +{ + if (UNEXPECTED(weakReferenceGetFn == NULL)) { + weakReferenceGetFn = pt_find_method(zend_ce_weakref, PT_LC("get")); + if (UNEXPECTED(weakReferenceGetFn == NULL)) return zv::Val(); + } + zval ret; + zend_call_known_function(weakReferenceGetFn, Z_OBJ_P(weakReference), Z_OBJCE_P(weakReference), &ret, 0, NULL, NULL); + if (UNEXPECTED(EG(exception))) { + zval_ptr_dtor(&ret); + return zv::Val(); + } + return zv::Val::adopt(ret); +} + +/* $storageRef->get()?->findExpressionResult($expr) — PHP null when the storage + * is gone or holds no result for the call */ +inline zv::Val findCallResultThroughWeakStorage(zval *storageRef, zval *expr) +{ + zv::Val callStorage = weakReferenceGet(storageRef); + if (UNEXPECTED(callStorage.isUndef())) return zv::Val(); + if (callStorage.isNull()) return zv::Val::null(); + return pt_expression_result_storage_find(callStorage.raw(), expr); +} + /* the ArgumentCountError of a native closure body called with too few * arguments; false = raised */ [[nodiscard]] inline bool requireArguments(uint32_t argc, uint32_t required, const char *closureName) diff --git a/turbo-ext/src/FuncCallHandler.cpp b/turbo-ext/src/FuncCallHandler.cpp index e47625e64ad..b76cc840b94 100644 --- a/turbo-ext/src/FuncCallHandler.cpp +++ b/turbo-ext/src/FuncCallHandler.cpp @@ -8,7 +8,8 @@ * entry (Engine.h). The twin's closures are native closures capturing what * the PHP closures capture: the typeCallback ($this, $nodeScopeResolver, * $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, - * $storageRef — a real WeakReference, as the twin holds), the early + * $storageRef — a real WeakReference, as the twin holds —, + * $hasConditionalAsserts), the early * terminating `static fn (bool) => new NeverType(true)`, the * specifyTypesCallback and createTypesCallback, array_walk()'s node gatherer * (its `&$arrayWalkValueTypes` a by-reference capture) and the mapTypes() @@ -204,40 +205,6 @@ zv::Val callPoly(PolySite &site, zval *object, const char *lcname, size_t len, c return zv::Val::adopt(ret); } -/* WeakReference::create($referent) / $weakReference->get() */ -zend_function *pt_fch_weakref_create_fn = NULL; -zend_function *pt_fch_weakref_get_fn = NULL; - -zv::Val weakReferenceCreate(zval *referent) -{ - if (UNEXPECTED(pt_fch_weakref_create_fn == NULL)) { - pt_fch_weakref_create_fn = pt_find_method(zend_ce_weakref, PT_LC("create")); - if (UNEXPECTED(pt_fch_weakref_create_fn == NULL)) return zv::Val(); - } - zval ret; - zend_call_known_function(pt_fch_weakref_create_fn, NULL, zend_ce_weakref, &ret, 1, referent, NULL); - if (UNEXPECTED(EG(exception))) { - zval_ptr_dtor(&ret); - return zv::Val(); - } - return zv::Val::adopt(ret); -} - -zv::Val weakReferenceGet(zval *weakReference) -{ - if (UNEXPECTED(pt_fch_weakref_get_fn == NULL)) { - pt_fch_weakref_get_fn = pt_find_method(zend_ce_weakref, PT_LC("get")); - if (UNEXPECTED(pt_fch_weakref_get_fn == NULL)) return zv::Val(); - } - zval ret; - zend_call_known_function(pt_fch_weakref_get_fn, Z_OBJ_P(weakReference), Z_OBJCE_P(weakReference), &ret, 0, NULL, NULL); - if (UNEXPECTED(EG(exception))) { - zval_ptr_dtor(&ret); - return zv::Val(); - } - return zv::Val::adopt(ret); -} - /* }}} */ /* {{{ node reads */ @@ -1105,11 +1072,18 @@ class FuncCallHandler // (native-types-promoted, on-demand / synthetic pricing, or special cases // inside resolveReturnType), the acceptor is re-derived from the // already-processed argument results on the asking scope. - zv::Val storageRef = weakReferenceCreate(storage); + zv::Val storageRef = ptcall::weakReferenceCreate(storage); if (UNEXPECTED(storageRef.isUndef())) return zv::Val(); + bool hasConditionalAsserts = false; + if (!functionReflection.isNull()) { + zv::Val assertsHold; + zval *assertions = pt_function_reflection_asserts(functionReflection.raw(), assertsHold); + if (UNEXPECTED(assertions == NULL)) return zv::Val(); + if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions, hasConditionalAsserts))) return zv::Val(); + } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&neverTypeCallbackBody) - : pt_native_closure(&typeCallbackBody, self, nodeScopeResolver, beforeScope, expr, nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw(), storageRef.raw()); + : pt_native_closure(&typeCallbackBody, self, nodeScopeResolver, beforeScope, expr, nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw(), storageRef.raw(), hasConditionalAsserts); zv::Val specifyTypesCallback = pt_native_closure(&specifyTypesCallbackBody, self, beforeScope, expr, normalizedExpr.raw(), nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw()); // A type constraint on a (narrowable, i.e. non-side-effecting, non-first-class) @@ -1963,7 +1937,8 @@ class FuncCallHandler /* function (bool $nativeTypesPromoted) use ($nodeScopeResolver, * $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, - * $argsResult, $storageRef): Type — captures: $this, then those */ + * $argsResult, $storageRef, $hasConditionalAsserts): Type — captures: $this, + * then those */ static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 1)) { @@ -1984,6 +1959,7 @@ class FuncCallHandler zval *resolvedParametersAcceptor = &captures[5]; zval *argsResult = &captures[6]; zval *storageRef = &captures[7]; + bool hasConditionalAsserts = Z_TYPE(captures[8]) == IS_TRUE; // for always-true/always-false type checks the call's own narrowing // (already produced as this result's specifyTypesCallback) decides @@ -2002,7 +1978,7 @@ class FuncCallHandler || zend_string_equals_literal_ci(nameString, "key_exists") || zend_string_equals_literal_ci(nameString, "in_array") || zend_string_equals_literal_ci(nameString, "is_subclass_of")) { - zv::Val callStorage = weakReferenceGet(storageRef); + zv::Val callStorage = ptcall::weakReferenceGet(storageRef); if (UNEXPECTED(callStorage.isUndef())) return zv::Val(); zv::Val callResult = zv::Val::null(); if (!callStorage.isNull()) { @@ -2023,7 +1999,15 @@ class FuncCallHandler if (UNEXPECTED(EG(exception))) return zv::Val(); } - return resolveReturnType(nodeScopeResolver, beforeScope, nativeTypesPromoted, expr, nameResult, nativeTypesPromoted || Z_TYPE_P(resolvedParametersAcceptor) == IS_NULL ? NULL : resolvedParametersAcceptor, argsResult); + zv::Val returnType = resolveReturnType(nodeScopeResolver, beforeScope, nativeTypesPromoted, expr, nameResult, nativeTypesPromoted || Z_TYPE_P(resolvedParametersAcceptor) == IS_NULL ? NULL : resolvedParametersAcceptor, argsResult); + if (UNEXPECTED(returnType.isUndef())) return zv::Val(); + if (nativeTypesPromoted || !hasConditionalAsserts) { + return returnType; + } + + zv::Val callResult = ptcall::findCallResultThroughWeakStorage(storageRef, expr); + if (UNEXPECTED(callResult.isUndef())) return zv::Val(); + return ptcall::resolveConditionalAssertReturnType(slot(slots::impossibleCheckTypeHelper), beforeScope, expr, callResult.raw(), argsResult, returnType.raw()); } /* the `$getType` closure of resolveReturnType() */ diff --git a/turbo-ext/src/MethodCallHandler.cpp b/turbo-ext/src/MethodCallHandler.cpp index 7a32051e52c..4922255d973 100644 --- a/turbo-ext/src/MethodCallHandler.cpp +++ b/turbo-ext/src/MethodCallHandler.cpp @@ -6,7 +6,8 @@ * arginfo so Nette autowires it. processExpr() is registered as the class's * handler entry (Engine.h). The twin's closures are native closures capturing * what the PHP closures capture: the typeCallback ($this, $beforeScope, - * $expr, $varResult, $nameResult, $resolvedParametersAcceptor, $argsResult; + * $expr, $varResult, $nameResult, $resolvedParametersAcceptor, $argsResult, + * $storageRef — a real WeakReference, as the twin holds —, $hasConditionalAsserts; * none for the early-terminating `new NeverType(true)` one), the * specifyTypesCallback ($this, $beforeScope, $expr, $normalizedExpr, * $varResult, $resolvedParametersAcceptor, $argsResult), the @@ -138,10 +139,10 @@ class MethodCallHandler /* the constructor body: the promoted properties */ void construct(zval **services, bool rememberPossiblyImpureFunctionValues) const { - static const uint32_t serviceSlots[10] = { + static const uint32_t serviceSlots[11] = { slots::calledMethodProcessor, slots::methodCallReturnTypeHelper, slots::methodThrowPointHelper, slots::reflectionProvider, slots::expressionResultFactory, slots::typeSpecifier, slots::defaultNarrowingHelper, slots::storagePrimer, - slots::earlyTerminatingHelper, slots::argumentsHandler, + slots::earlyTerminatingHelper, slots::argumentsHandler, slots::impossibleCheckTypeHelper, }; for (uint32_t i = 0; i < 4; i++) { pt_write_slot(self, serviceSlots[i], services[i]); @@ -149,7 +150,7 @@ class MethodCallHandler zval remember = {}; ZVAL_BOOL(&remember, rememberPossiblyImpureFunctionValues); pt_write_slot(self, slots::rememberPossiblyImpureFunctionValues, &remember); - for (uint32_t i = 4; i < 10; i++) { + for (uint32_t i = 4; i < 11; i++) { pt_write_slot(self, serviceSlots[i], services[i]); } } @@ -394,9 +395,17 @@ class MethodCallHandler // evolving scope (type-driven, generics resolved). When null // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. + zv::Val storageRef = weakReferenceCreate(storage); + if (UNEXPECTED(storageRef.isUndef())) return zv::Val(); + bool hasConditionalAsserts = false; + if (!methodReflection.isNull()) { + zv::Val assertions = pt_extended_method_reflection_call(methodReflection.raw(), PT_MR_GET_ASSERTS); + if (UNEXPECTED(assertions.isUndef())) return zv::Val(); + if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); + } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&earlyTerminatingTypeCallbackBody) - : pt_native_closure(&typeCallbackBody, self, beforeScope, expr, varResult.raw(), nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw()); + : pt_native_closure(&typeCallbackBody, self, beforeScope, expr, varResult.raw(), nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw(), storageRef.raw(), hasConditionalAsserts); zv::Val specifyTypesCallback = pt_native_closure(&specifyTypesCallbackBody, self, beforeScope, expr, normalizedExpr.raw(), varResult.raw(), resolvedParametersAcceptor.raw(), methodReflection.raw(), argsResult.raw()); // A type constraint on a (narrowable, i.e. non-side-effecting) method call // narrows the call itself - the inside-out equivalent of createForExpr's @@ -1034,11 +1043,9 @@ class MethodCallHandler ZVAL_COPY_VALUE(return_value, &never); } - /* fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType($beforeScope, - * $nativeTypesPromoted, $expr, $varResult, $nameResult, $nativeTypesPromoted ? - * null : $resolvedParametersAcceptor, $argsResult) — captures: $this, - * $beforeScope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor, - * $argsResult */ + /* function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $varResult, + * $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, + * $hasConditionalAsserts): Type — captures: $this, then those */ static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(argc < 1)) { @@ -1048,6 +1055,15 @@ class MethodCallHandler bool nativeTypesPromoted = zend_is_true(&argv[0]); zv::Val type = MethodCallHandler(Z_OBJ(captures[0])).resolveReturnType(&captures[1], nativeTypesPromoted, &captures[2], &captures[3], &captures[4], nativeTypesPromoted ? NULL : &captures[5], &captures[6]); if (UNEXPECTED(type.isUndef())) return; + if (!nativeTypesPromoted && Z_TYPE(captures[8]) == IS_TRUE) { + // the call's own narrowing decides a conditional @phpstan-assert; + // its result is looked up through a weak storage reference, as + // this callback is owned by that very result + zv::Val callResult = findCallResultThroughWeakStorage(&captures[7], &captures[2]); + if (UNEXPECTED(callResult.isUndef())) return; + type = resolveConditionalAssertReturnType(OBJ_PROP_NUM(Z_OBJ(captures[0]), slots::impossibleCheckTypeHelper), &captures[1], &captures[2], callResult.raw(), &captures[6], type.raw()); + } + if (UNEXPECTED(type.isUndef())) return; type.intoReturnValue(return_value); } @@ -1147,9 +1163,9 @@ PT_MINIT_REGISTRATION(pt_register_method_call_handler) /* the real parameter class names: the DI container autowires the * service by reflecting the constructor */ cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *calledMethodProcessor, *methodCallReturnTypeHelper, *methodThrowPointHelper, *reflectionProvider, *expressionResultFactory, *typeSpecifier, *defaultNarrowingHelper, *storagePrimer, *earlyTerminatingHelper, *argumentsHandler; + zval *calledMethodProcessor, *methodCallReturnTypeHelper, *methodThrowPointHelper, *reflectionProvider, *expressionResultFactory, *typeSpecifier, *defaultNarrowingHelper, *storagePrimer, *earlyTerminatingHelper, *argumentsHandler, *impossibleCheckTypeHelper; bool rememberPossiblyImpureFunctionValues; - ZEND_PARSE_PARAMETERS_START(11, 11) + ZEND_PARSE_PARAMETERS_START(12, 12) Z_PARAM_OBJECT(calledMethodProcessor) Z_PARAM_OBJECT(methodCallReturnTypeHelper) Z_PARAM_OBJECT(methodThrowPointHelper) @@ -1161,8 +1177,9 @@ PT_MINIT_REGISTRATION(pt_register_method_call_handler) Z_PARAM_OBJECT(storagePrimer) Z_PARAM_OBJECT(earlyTerminatingHelper) Z_PARAM_OBJECT(argumentsHandler) + Z_PARAM_OBJECT(impossibleCheckTypeHelper) ZEND_PARSE_PARAMETERS_END(); - zval *services[10] = {calledMethodProcessor, methodCallReturnTypeHelper, methodThrowPointHelper, reflectionProvider, expressionResultFactory, typeSpecifier, defaultNarrowingHelper, storagePrimer, earlyTerminatingHelper, argumentsHandler}; + zval *services[11] = {calledMethodProcessor, methodCallReturnTypeHelper, methodThrowPointHelper, reflectionProvider, expressionResultFactory, typeSpecifier, defaultNarrowingHelper, storagePrimer, earlyTerminatingHelper, argumentsHandler, impossibleCheckTypeHelper}; MethodCallHandler(Z_OBJ_P(ZEND_THIS)).construct(services, rememberPossiblyImpureFunctionValues); }); diff --git a/turbo-ext/src/StaticCallHandler.cpp b/turbo-ext/src/StaticCallHandler.cpp index b951a7df69d..97b644af493 100644 --- a/turbo-ext/src/StaticCallHandler.cpp +++ b/turbo-ext/src/StaticCallHandler.cpp @@ -8,7 +8,8 @@ * what the PHP closures capture: the Closure::bind() scope factory handed to * processArgs() ($expr, $storage; its inner $readArgType closure, created and * called only inside it, is inlined), the typeCallback ($this, $beforeScope, - * $expr, $classResult, $nameResult, $resolvedParametersAcceptor, $argsResult; + * $expr, $classResult, $nameResult, $resolvedParametersAcceptor, $argsResult, + * $storageRef — a real WeakReference, as the twin holds —, $hasConditionalAsserts; * none for the early-terminating `new NeverType(true)` one), the * specifyTypesCallback ($this, $beforeScope, $expr, $normalizedExpr, * $classResult, $resolvedParametersAcceptor, $argsResult), the @@ -294,10 +295,10 @@ class StaticCallHandler /* the constructor body: the promoted properties */ void construct(zval **services, bool rememberPossiblyImpureFunctionValues) const { - static const uint32_t serviceSlots[9] = { + static const uint32_t serviceSlots[10] = { slots::methodCallReturnTypeHelper, slots::methodThrowPointHelper, slots::reflectionProvider, slots::expressionResultFactory, slots::typeSpecifier, slots::defaultNarrowingHelper, slots::storagePrimer, - slots::earlyTerminatingHelper, slots::argumentsHandler, + slots::earlyTerminatingHelper, slots::argumentsHandler, slots::impossibleCheckTypeHelper, }; for (uint32_t i = 0; i < 3; i++) { pt_write_slot(self, serviceSlots[i], services[i]); @@ -305,7 +306,7 @@ class StaticCallHandler zval remember = {}; ZVAL_BOOL(&remember, rememberPossiblyImpureFunctionValues); pt_write_slot(self, slots::rememberPossiblyImpureFunctionValues, &remember); - for (uint32_t i = 3; i < 9; i++) { + for (uint32_t i = 3; i < 10; i++) { pt_write_slot(self, serviceSlots[i], services[i]); } } @@ -637,9 +638,17 @@ class StaticCallHandler // evolving scope (type-driven, generics resolved). When null // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. + zv::Val storageRef = weakReferenceCreate(storage); + if (UNEXPECTED(storageRef.isUndef())) return zv::Val(); + bool hasConditionalAsserts = false; + if (!methodReflection.isNull()) { + zv::Val assertions = pt_extended_method_reflection_call(methodReflection.raw(), PT_MR_GET_ASSERTS); + if (UNEXPECTED(assertions.isUndef())) return zv::Val(); + if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); + } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&earlyTerminatingTypeCallbackBody) - : pt_native_closure(&typeCallbackBody, self, beforeScope, expr, classResult.raw(), nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw()); + : pt_native_closure(&typeCallbackBody, self, beforeScope, expr, classResult.raw(), nameResult.raw(), resolvedParametersAcceptor.raw(), argsResult.raw(), storageRef.raw(), hasConditionalAsserts); zv::Val specifyTypesCallback = pt_native_closure(&specifyTypesCallbackBody, self, beforeScope, expr, normalizedExpr.raw(), classResult.raw(), resolvedParametersAcceptor.raw(), methodReflection.raw(), argsResult.raw()); // A type constraint on a (narrowable, i.e. non-side-effecting) static call // narrows the call itself - the inside-out equivalent of createForExpr's @@ -1415,17 +1424,24 @@ class StaticCallHandler ZVAL_COPY_VALUE(return_value, &never); } - /* fn (bool $nativeTypesPromoted): Type => $this->resolveReturnType($beforeScope, - * $nativeTypesPromoted, $expr, $classResult, $nameResult, $nativeTypesPromoted ? - * null : $resolvedParametersAcceptor, $argsResult) — captures: $this, - * $beforeScope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor, - * $argsResult */ + /* function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $classResult, + * $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, + * $hasConditionalAsserts): Type — captures: $this, then those */ static void typeCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(!requireArguments(argc, 1, "PHPStan\\Analyser\\ExprHandler\\StaticCallHandler::{closure}"))) return; bool nativeTypesPromoted = zend_is_true(&argv[0]); zv::Val type = StaticCallHandler(Z_OBJ(captures[0])).resolveReturnType(&captures[1], nativeTypesPromoted, &captures[2], &captures[3], &captures[4], nativeTypesPromoted ? NULL : &captures[5], &captures[6]); if (UNEXPECTED(type.isUndef())) return; + if (!nativeTypesPromoted && Z_TYPE(captures[8]) == IS_TRUE) { + // the call's own narrowing decides a conditional @phpstan-assert; + // its result is looked up through a weak storage reference, as + // this callback is owned by that very result + zv::Val callResult = findCallResultThroughWeakStorage(&captures[7], &captures[2]); + if (UNEXPECTED(callResult.isUndef())) return; + type = resolveConditionalAssertReturnType(OBJ_PROP_NUM(Z_OBJ(captures[0]), slots::impossibleCheckTypeHelper), &captures[1], &captures[2], callResult.raw(), &captures[6], type.raw()); + } + if (UNEXPECTED(type.isUndef())) return; type.intoReturnValue(return_value); } @@ -1507,9 +1523,9 @@ PT_MINIT_REGISTRATION(pt_register_static_call_handler) /* the real parameter class names: the DI container autowires the * service by reflecting the constructor */ cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *methodCallReturnTypeHelper, *methodThrowPointHelper, *reflectionProvider, *expressionResultFactory, *typeSpecifier, *defaultNarrowingHelper, *storagePrimer, *earlyTerminatingHelper, *argumentsHandler; + zval *methodCallReturnTypeHelper, *methodThrowPointHelper, *reflectionProvider, *expressionResultFactory, *typeSpecifier, *defaultNarrowingHelper, *storagePrimer, *earlyTerminatingHelper, *argumentsHandler, *impossibleCheckTypeHelper; bool rememberPossiblyImpureFunctionValues; - ZEND_PARSE_PARAMETERS_START(10, 10) + ZEND_PARSE_PARAMETERS_START(11, 11) Z_PARAM_OBJECT(methodCallReturnTypeHelper) Z_PARAM_OBJECT(methodThrowPointHelper) Z_PARAM_OBJECT(reflectionProvider) @@ -1520,8 +1536,9 @@ PT_MINIT_REGISTRATION(pt_register_static_call_handler) Z_PARAM_OBJECT(storagePrimer) Z_PARAM_OBJECT(earlyTerminatingHelper) Z_PARAM_OBJECT(argumentsHandler) + Z_PARAM_OBJECT(impossibleCheckTypeHelper) ZEND_PARSE_PARAMETERS_END(); - zval *services[9] = {methodCallReturnTypeHelper, methodThrowPointHelper, reflectionProvider, expressionResultFactory, typeSpecifier, defaultNarrowingHelper, storagePrimer, earlyTerminatingHelper, argumentsHandler}; + zval *services[10] = {methodCallReturnTypeHelper, methodThrowPointHelper, reflectionProvider, expressionResultFactory, typeSpecifier, defaultNarrowingHelper, storagePrimer, earlyTerminatingHelper, argumentsHandler, impossibleCheckTypeHelper}; StaticCallHandler(Z_OBJ_P(ZEND_THIS)).construct(services, rememberPossiblyImpureFunctionValues); }); diff --git a/turbo-ext/src/generated/MethodCallHandler.h b/turbo-ext/src/generated/MethodCallHandler.h index 2950b6cbbcb..3e5298fca57 100644 --- a/turbo-ext/src/generated/MethodCallHandler.h +++ b/turbo-ext/src/generated/MethodCallHandler.h @@ -21,6 +21,7 @@ inline constexpr uint32_t defaultNarrowingHelper = 7; inline constexpr uint32_t storagePrimer = 8; inline constexpr uint32_t earlyTerminatingHelper = 9; inline constexpr uint32_t argumentsHandler = 10; +inline constexpr uint32_t impossibleCheckTypeHelper = 11; } // namespace slot inline void declareClass(reg::Class &cls) @@ -43,6 +44,7 @@ inline void declareProperties(reg::Class &cls) cls.property("storagePrimer", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DynamicReturnTypeStoragePrimer"); cls.property("earlyTerminatingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\EarlyTerminatingCallHelper"); cls.property("argumentsHandler", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ArgumentsHandler"); + cls.property("impossibleCheckTypeHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeHelper"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -69,45 +71,47 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExprHandler\\Helper\\EarlyTerminatingCallHelper\0" /* 623 */ "argumentsHandler\0" /* 686 */ "PHPStan\\Analyser\\ArgumentsHandler\0" /* 703 */ - "__construct\0" /* 737 */ - "expr\0" /* 749 */ - "PhpParser\\Node\\Expr\0" /* 754 */ - "\0" /* 774 */ - "supports\0" /* 775 */ - "nodeScopeResolver\0" /* 784 */ - "PHPStan\\Analyser\\NodeScopeResolver\0" /* 802 */ - "stmt\0" /* 837 */ - "PhpParser\\Node\\Stmt\0" /* 842 */ - "scope\0" /* 862 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 868 */ - "storage\0" /* 899 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 907 */ - "nodeCallback\0" /* 948 */ - "context\0" /* 961 */ - "PHPStan\\Analyser\\ExpressionContext\0" /* 969 */ - "PHPStan\\Analyser\\ExpressionResult\0" /* 1004 */ - "processExpr\0" /* 1038 */ - "reflectionScope\0" /* 1050 */ - "nativeTypesPromoted\0" /* 1066 */ - "PhpParser\\Node\\Expr\\MethodCall\0" /* 1086 */ - "varResult\0" /* 1117 */ - "nameResult\0" /* 1127 */ - "preResolvedAcceptor\0" /* 1138 */ - "PHPStan\\Reflection\\ParametersAcceptor\0" /* 1158 */ - "argsResult\0" /* 1196 */ - "PHPStan\\Analyser\\ArgsResult\0" /* 1207 */ - "PHPStan\\Type\\Type\0" /* 1235 */ - "resolveReturnType\0" /* 1253 */ - "normalizedExpr\0" /* 1271 */ - "resolvedParametersAcceptor\0" /* 1286 */ - "walkMethodReflection\0" /* 1313 */ - "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 1334 */ - "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1378 */ - "null\0" /* 1416 */ - "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1421 */ - "specifyTypes\0" /* 1453 */ - "defaultMethodCallNarrowing\0" /* 1466 */ - "isMethodCallNarrowable"; /* 1493 */ + "impossibleCheckTypeHelper\0" /* 737 */ + "PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeHelper\0" /* 763 */ + "__construct\0" /* 814 */ + "expr\0" /* 826 */ + "PhpParser\\Node\\Expr\0" /* 831 */ + "\0" /* 851 */ + "supports\0" /* 852 */ + "nodeScopeResolver\0" /* 861 */ + "PHPStan\\Analyser\\NodeScopeResolver\0" /* 879 */ + "stmt\0" /* 914 */ + "PhpParser\\Node\\Stmt\0" /* 919 */ + "scope\0" /* 939 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 945 */ + "storage\0" /* 976 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 984 */ + "nodeCallback\0" /* 1025 */ + "context\0" /* 1038 */ + "PHPStan\\Analyser\\ExpressionContext\0" /* 1046 */ + "PHPStan\\Analyser\\ExpressionResult\0" /* 1081 */ + "processExpr\0" /* 1115 */ + "reflectionScope\0" /* 1127 */ + "nativeTypesPromoted\0" /* 1143 */ + "PhpParser\\Node\\Expr\\MethodCall\0" /* 1163 */ + "varResult\0" /* 1194 */ + "nameResult\0" /* 1204 */ + "preResolvedAcceptor\0" /* 1215 */ + "PHPStan\\Reflection\\ParametersAcceptor\0" /* 1235 */ + "argsResult\0" /* 1273 */ + "PHPStan\\Analyser\\ArgsResult\0" /* 1284 */ + "PHPStan\\Type\\Type\0" /* 1312 */ + "resolveReturnType\0" /* 1330 */ + "normalizedExpr\0" /* 1348 */ + "resolvedParametersAcceptor\0" /* 1363 */ + "walkMethodReflection\0" /* 1390 */ + "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 1411 */ + "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1455 */ + "null\0" /* 1493 */ + "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1498 */ + "specifyTypes\0" /* 1530 */ + "defaultMethodCallNarrowing\0" /* 1543 */ + "isMethodCallNarrowable"; /* 1570 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 22), /* __construct $calledMethodProcessor */ reg::packed(61, 0, 88), /* __construct $methodCallReturnTypeHelper */ @@ -120,55 +124,56 @@ inline constexpr reg::PackedArg args[] = { reg::packed(519, 0, 533), /* __construct $storagePrimer */ reg::packed(600, 0, 623), /* __construct $earlyTerminatingHelper */ reg::packed(686, 0, 703), /* __construct $argumentsHandler */ - reg::packed(749, 0, 754), /* supports $expr */ - reg::packed(774, MAY_BE_BOOL), /* supports return */ - reg::packed(784, 0, 802), /* processExpr $nodeScopeResolver */ - reg::packed(837, 0, 842), /* processExpr $stmt */ - reg::packed(749, 0, 754), /* processExpr $expr */ - reg::packed(862, 0, 868), /* processExpr $scope */ - reg::packed(899, 0, 907), /* processExpr $storage */ - reg::packed(948, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ - reg::packed(961, 0, 969), /* processExpr $context */ - reg::packed(774, 0, 1004), /* processExpr return */ - reg::packed(1050, 0, 868), /* resolveReturnType $reflectionScope */ - reg::packed(1066, MAY_BE_BOOL), /* resolveReturnType $nativeTypesPromoted */ - reg::packed(749, 0, 1086), /* resolveReturnType $expr */ - reg::packed(1117, 0, 1004), /* resolveReturnType $varResult */ - reg::packed(1127, MAY_BE_NULL, 1004), /* resolveReturnType $nameResult */ - reg::packed(1138, MAY_BE_NULL, 1158), /* resolveReturnType $preResolvedAcceptor */ - reg::packed(1196, MAY_BE_NULL, 1207), /* resolveReturnType $argsResult */ - reg::packed(774, 0, 1235), /* resolveReturnType return */ - reg::packed(862, 0, 868), /* specifyTypes $scope */ - reg::packed(749, 0, 754), /* specifyTypes $expr */ - reg::packed(1271, 0, 754), /* specifyTypes $normalizedExpr */ - reg::packed(1117, 0, 1004), /* specifyTypes $varResult */ - reg::packed(1286, MAY_BE_NULL, 1158), /* specifyTypes $resolvedParametersAcceptor */ - reg::packed(1313, MAY_BE_NULL, 1334), /* specifyTypes $walkMethodReflection */ - reg::packed(961, 0, 1378), /* specifyTypes $context */ - reg::packed(1196, MAY_BE_NULL, 1207, false, false, 1416), /* specifyTypes $argsResult */ - reg::packed(774, 0, 1421), /* specifyTypes return */ - reg::packed(862, 0, 868), /* defaultMethodCallNarrowing $scope */ - reg::packed(749, 0, 754), /* defaultMethodCallNarrowing $expr */ - reg::packed(1117, 0, 1004), /* defaultMethodCallNarrowing $varResult */ - reg::packed(961, 0, 1378), /* defaultMethodCallNarrowing $context */ - reg::packed(774, 0, 1421), /* defaultMethodCallNarrowing return */ - reg::packed(862, 0, 868), /* isMethodCallNarrowable $scope */ - reg::packed(749, 0, 754), /* isMethodCallNarrowable $expr */ - reg::packed(1117, 0, 1004), /* isMethodCallNarrowable $varResult */ - reg::packed(774, MAY_BE_BOOL), /* isMethodCallNarrowable return */ + reg::packed(737, 0, 763), /* __construct $impossibleCheckTypeHelper */ + reg::packed(826, 0, 831), /* supports $expr */ + reg::packed(851, MAY_BE_BOOL), /* supports return */ + reg::packed(861, 0, 879), /* processExpr $nodeScopeResolver */ + reg::packed(914, 0, 919), /* processExpr $stmt */ + reg::packed(826, 0, 831), /* processExpr $expr */ + reg::packed(939, 0, 945), /* processExpr $scope */ + reg::packed(976, 0, 984), /* processExpr $storage */ + reg::packed(1025, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ + reg::packed(1038, 0, 1046), /* processExpr $context */ + reg::packed(851, 0, 1081), /* processExpr return */ + reg::packed(1127, 0, 945), /* resolveReturnType $reflectionScope */ + reg::packed(1143, MAY_BE_BOOL), /* resolveReturnType $nativeTypesPromoted */ + reg::packed(826, 0, 1163), /* resolveReturnType $expr */ + reg::packed(1194, 0, 1081), /* resolveReturnType $varResult */ + reg::packed(1204, MAY_BE_NULL, 1081), /* resolveReturnType $nameResult */ + reg::packed(1215, MAY_BE_NULL, 1235), /* resolveReturnType $preResolvedAcceptor */ + reg::packed(1273, MAY_BE_NULL, 1284), /* resolveReturnType $argsResult */ + reg::packed(851, 0, 1312), /* resolveReturnType return */ + reg::packed(939, 0, 945), /* specifyTypes $scope */ + reg::packed(826, 0, 831), /* specifyTypes $expr */ + reg::packed(1348, 0, 831), /* specifyTypes $normalizedExpr */ + reg::packed(1194, 0, 1081), /* specifyTypes $varResult */ + reg::packed(1363, MAY_BE_NULL, 1235), /* specifyTypes $resolvedParametersAcceptor */ + reg::packed(1390, MAY_BE_NULL, 1411), /* specifyTypes $walkMethodReflection */ + reg::packed(1038, 0, 1455), /* specifyTypes $context */ + reg::packed(1273, MAY_BE_NULL, 1284, false, false, 1493), /* specifyTypes $argsResult */ + reg::packed(851, 0, 1498), /* specifyTypes return */ + reg::packed(939, 0, 945), /* defaultMethodCallNarrowing $scope */ + reg::packed(826, 0, 831), /* defaultMethodCallNarrowing $expr */ + reg::packed(1194, 0, 1081), /* defaultMethodCallNarrowing $varResult */ + reg::packed(1038, 0, 1455), /* defaultMethodCallNarrowing $context */ + reg::packed(851, 0, 1498), /* defaultMethodCallNarrowing return */ + reg::packed(939, 0, 945), /* isMethodCallNarrowable $scope */ + reg::packed(826, 0, 831), /* isMethodCallNarrowable $expr */ + reg::packed(1194, 0, 1081), /* isMethodCallNarrowable $varResult */ + reg::packed(851, MAY_BE_BOOL), /* isMethodCallNarrowable return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 737 /* __construct */, 11, 0, 11, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig supports = { { 775 /* supports */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processExpr = { { 1038 /* processExpr */, 7, 13, 7, 20, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveReturnType = { { 1253 /* resolveReturnType */, 7, 21, 7, 28, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig specifyTypes = { { 1453 /* specifyTypes */, 7, 29, 8, 37, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig defaultMethodCallNarrowing = { { 1466 /* defaultMethodCallNarrowing */, 4, 38, 4, 42, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isMethodCallNarrowable = { { 1493 /* isMethodCallNarrowable */, 3, 43, 3, 46, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig __construct = { { 814 /* __construct */, 12, 0, 12, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig supports = { { 852 /* supports */, 1, 12, 1, 13, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processExpr = { { 1115 /* processExpr */, 7, 14, 7, 21, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveReturnType = { { 1330 /* resolveReturnType */, 7, 22, 7, 29, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyTypes = { { 1530 /* specifyTypes */, 7, 30, 8, 38, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig defaultMethodCallNarrowing = { { 1543 /* defaultMethodCallNarrowing */, 4, 39, 4, 43, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isMethodCallNarrowable = { { 1570 /* isMethodCallNarrowable */, 3, 44, 3, 47, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::MethodCallHandler diff --git a/turbo-ext/src/generated/StaticCallHandler.h b/turbo-ext/src/generated/StaticCallHandler.h index fa4fb3b8676..ed2f9062805 100644 --- a/turbo-ext/src/generated/StaticCallHandler.h +++ b/turbo-ext/src/generated/StaticCallHandler.h @@ -20,6 +20,7 @@ inline constexpr uint32_t defaultNarrowingHelper = 6; inline constexpr uint32_t storagePrimer = 7; inline constexpr uint32_t earlyTerminatingHelper = 8; inline constexpr uint32_t argumentsHandler = 9; +inline constexpr uint32_t impossibleCheckTypeHelper = 10; } // namespace slot inline void declareClass(reg::Class &cls) @@ -41,6 +42,7 @@ inline void declareProperties(reg::Class &cls) cls.property("storagePrimer", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DynamicReturnTypeStoragePrimer"); cls.property("earlyTerminatingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\EarlyTerminatingCallHelper"); cls.property("argumentsHandler", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ArgumentsHandler"); + cls.property("impossibleCheckTypeHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeHelper"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -65,50 +67,52 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExprHandler\\Helper\\EarlyTerminatingCallHelper\0" /* 562 */ "argumentsHandler\0" /* 625 */ "PHPStan\\Analyser\\ArgumentsHandler\0" /* 642 */ - "__construct\0" /* 676 */ - "expr\0" /* 688 */ - "PhpParser\\Node\\Expr\0" /* 693 */ - "\0" /* 713 */ - "supports\0" /* 714 */ - "nodeScopeResolver\0" /* 723 */ - "PHPStan\\Analyser\\NodeScopeResolver\0" /* 741 */ - "stmt\0" /* 776 */ - "PhpParser\\Node\\Stmt\0" /* 781 */ - "scope\0" /* 801 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 807 */ - "storage\0" /* 838 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 846 */ - "nodeCallback\0" /* 887 */ - "context\0" /* 900 */ - "PHPStan\\Analyser\\ExpressionContext\0" /* 908 */ - "PHPStan\\Analyser\\ExpressionResult\0" /* 943 */ - "processExpr\0" /* 977 */ - "reflectionScope\0" /* 989 */ - "nativeTypesPromoted\0" /* 1005 */ - "PhpParser\\Node\\Expr\\StaticCall\0" /* 1025 */ - "classResult\0" /* 1056 */ - "nameResult\0" /* 1068 */ - "preResolvedAcceptor\0" /* 1079 */ - "PHPStan\\Reflection\\ParametersAcceptor\0" /* 1099 */ - "argsResult\0" /* 1137 */ - "PHPStan\\Analyser\\ArgsResult\0" /* 1148 */ - "PHPStan\\Type\\Type\0" /* 1176 */ - "resolveReturnType\0" /* 1194 */ - "normalizedExpr\0" /* 1212 */ - "resolvedParametersAcceptor\0" /* 1227 */ - "walkMethodReflection\0" /* 1254 */ - "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 1275 */ - "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1319 */ - "null\0" /* 1357 */ - "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1362 */ - "specifyTypes\0" /* 1394 */ - "defaultStaticCallNarrowing\0" /* 1407 */ - "isStaticCallNarrowable\0" /* 1434 */ - "class\0" /* 1457 */ - "PhpParser\\Node\\Name\0" /* 1463 */ - "methodName\0" /* 1483 */ - "PHPStan\\Type\\TypeWithClassName\0" /* 1494 */ - "resolveTypeByNameWithLateStaticBinding"; /* 1525 */ + "impossibleCheckTypeHelper\0" /* 676 */ + "PHPStan\\Rules\\Comparison\\ImpossibleCheckTypeHelper\0" /* 702 */ + "__construct\0" /* 753 */ + "expr\0" /* 765 */ + "PhpParser\\Node\\Expr\0" /* 770 */ + "\0" /* 790 */ + "supports\0" /* 791 */ + "nodeScopeResolver\0" /* 800 */ + "PHPStan\\Analyser\\NodeScopeResolver\0" /* 818 */ + "stmt\0" /* 853 */ + "PhpParser\\Node\\Stmt\0" /* 858 */ + "scope\0" /* 878 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 884 */ + "storage\0" /* 915 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 923 */ + "nodeCallback\0" /* 964 */ + "context\0" /* 977 */ + "PHPStan\\Analyser\\ExpressionContext\0" /* 985 */ + "PHPStan\\Analyser\\ExpressionResult\0" /* 1020 */ + "processExpr\0" /* 1054 */ + "reflectionScope\0" /* 1066 */ + "nativeTypesPromoted\0" /* 1082 */ + "PhpParser\\Node\\Expr\\StaticCall\0" /* 1102 */ + "classResult\0" /* 1133 */ + "nameResult\0" /* 1145 */ + "preResolvedAcceptor\0" /* 1156 */ + "PHPStan\\Reflection\\ParametersAcceptor\0" /* 1176 */ + "argsResult\0" /* 1214 */ + "PHPStan\\Analyser\\ArgsResult\0" /* 1225 */ + "PHPStan\\Type\\Type\0" /* 1253 */ + "resolveReturnType\0" /* 1271 */ + "normalizedExpr\0" /* 1289 */ + "resolvedParametersAcceptor\0" /* 1304 */ + "walkMethodReflection\0" /* 1331 */ + "PHPStan\\Reflection\\ExtendedMethodReflection\0" /* 1352 */ + "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1396 */ + "null\0" /* 1434 */ + "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1439 */ + "specifyTypes\0" /* 1471 */ + "defaultStaticCallNarrowing\0" /* 1484 */ + "isStaticCallNarrowable\0" /* 1511 */ + "class\0" /* 1534 */ + "PhpParser\\Node\\Name\0" /* 1540 */ + "methodName\0" /* 1560 */ + "PHPStan\\Type\\TypeWithClassName\0" /* 1571 */ + "resolveTypeByNameWithLateStaticBinding"; /* 1602 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 27), /* __construct $methodCallReturnTypeHelper */ reg::packed(90, 0, 113), /* __construct $methodThrowPointHelper */ @@ -120,60 +124,61 @@ inline constexpr reg::PackedArg args[] = { reg::packed(458, 0, 472), /* __construct $storagePrimer */ reg::packed(539, 0, 562), /* __construct $earlyTerminatingHelper */ reg::packed(625, 0, 642), /* __construct $argumentsHandler */ - reg::packed(688, 0, 693), /* supports $expr */ - reg::packed(713, MAY_BE_BOOL), /* supports return */ - reg::packed(723, 0, 741), /* processExpr $nodeScopeResolver */ - reg::packed(776, 0, 781), /* processExpr $stmt */ - reg::packed(688, 0, 693), /* processExpr $expr */ - reg::packed(801, 0, 807), /* processExpr $scope */ - reg::packed(838, 0, 846), /* processExpr $storage */ - reg::packed(887, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ - reg::packed(900, 0, 908), /* processExpr $context */ - reg::packed(713, 0, 943), /* processExpr return */ - reg::packed(989, 0, 807), /* resolveReturnType $reflectionScope */ - reg::packed(1005, MAY_BE_BOOL), /* resolveReturnType $nativeTypesPromoted */ - reg::packed(688, 0, 1025), /* resolveReturnType $expr */ - reg::packed(1056, MAY_BE_NULL, 943), /* resolveReturnType $classResult */ - reg::packed(1068, MAY_BE_NULL, 943), /* resolveReturnType $nameResult */ - reg::packed(1079, MAY_BE_NULL, 1099), /* resolveReturnType $preResolvedAcceptor */ - reg::packed(1137, MAY_BE_NULL, 1148), /* resolveReturnType $argsResult */ - reg::packed(713, 0, 1176), /* resolveReturnType return */ - reg::packed(801, 0, 807), /* specifyTypes $scope */ - reg::packed(688, 0, 693), /* specifyTypes $expr */ - reg::packed(1212, 0, 693), /* specifyTypes $normalizedExpr */ - reg::packed(1056, MAY_BE_NULL, 943), /* specifyTypes $classResult */ - reg::packed(1227, MAY_BE_NULL, 1099), /* specifyTypes $resolvedParametersAcceptor */ - reg::packed(1254, MAY_BE_NULL, 1275), /* specifyTypes $walkMethodReflection */ - reg::packed(900, 0, 1319), /* specifyTypes $context */ - reg::packed(1137, MAY_BE_NULL, 1148, false, false, 1357), /* specifyTypes $argsResult */ - reg::packed(713, 0, 1362), /* specifyTypes return */ - reg::packed(801, 0, 807), /* defaultStaticCallNarrowing $scope */ - reg::packed(688, 0, 693), /* defaultStaticCallNarrowing $expr */ - reg::packed(1056, MAY_BE_NULL, 943), /* defaultStaticCallNarrowing $classResult */ - reg::packed(900, 0, 1319), /* defaultStaticCallNarrowing $context */ - reg::packed(713, 0, 1362), /* defaultStaticCallNarrowing return */ - reg::packed(801, 0, 807), /* isStaticCallNarrowable $scope */ - reg::packed(688, 0, 693), /* isStaticCallNarrowable $expr */ - reg::packed(1056, MAY_BE_NULL, 943), /* isStaticCallNarrowable $classResult */ - reg::packed(713, MAY_BE_BOOL), /* isStaticCallNarrowable return */ - reg::packed(801, 0, 807), /* resolveTypeByNameWithLateStaticBinding $scope */ - reg::packed(1457, 0, 1463), /* resolveTypeByNameWithLateStaticBinding $class */ - reg::packed(1483, MAY_BE_STRING), /* resolveTypeByNameWithLateStaticBinding $methodName */ - reg::packed(713, 0, 1494), /* resolveTypeByNameWithLateStaticBinding return */ + reg::packed(676, 0, 702), /* __construct $impossibleCheckTypeHelper */ + reg::packed(765, 0, 770), /* supports $expr */ + reg::packed(790, MAY_BE_BOOL), /* supports return */ + reg::packed(800, 0, 818), /* processExpr $nodeScopeResolver */ + reg::packed(853, 0, 858), /* processExpr $stmt */ + reg::packed(765, 0, 770), /* processExpr $expr */ + reg::packed(878, 0, 884), /* processExpr $scope */ + reg::packed(915, 0, 923), /* processExpr $storage */ + reg::packed(964, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ + reg::packed(977, 0, 985), /* processExpr $context */ + reg::packed(790, 0, 1020), /* processExpr return */ + reg::packed(1066, 0, 884), /* resolveReturnType $reflectionScope */ + reg::packed(1082, MAY_BE_BOOL), /* resolveReturnType $nativeTypesPromoted */ + reg::packed(765, 0, 1102), /* resolveReturnType $expr */ + reg::packed(1133, MAY_BE_NULL, 1020), /* resolveReturnType $classResult */ + reg::packed(1145, MAY_BE_NULL, 1020), /* resolveReturnType $nameResult */ + reg::packed(1156, MAY_BE_NULL, 1176), /* resolveReturnType $preResolvedAcceptor */ + reg::packed(1214, MAY_BE_NULL, 1225), /* resolveReturnType $argsResult */ + reg::packed(790, 0, 1253), /* resolveReturnType return */ + reg::packed(878, 0, 884), /* specifyTypes $scope */ + reg::packed(765, 0, 770), /* specifyTypes $expr */ + reg::packed(1289, 0, 770), /* specifyTypes $normalizedExpr */ + reg::packed(1133, MAY_BE_NULL, 1020), /* specifyTypes $classResult */ + reg::packed(1304, MAY_BE_NULL, 1176), /* specifyTypes $resolvedParametersAcceptor */ + reg::packed(1331, MAY_BE_NULL, 1352), /* specifyTypes $walkMethodReflection */ + reg::packed(977, 0, 1396), /* specifyTypes $context */ + reg::packed(1214, MAY_BE_NULL, 1225, false, false, 1434), /* specifyTypes $argsResult */ + reg::packed(790, 0, 1439), /* specifyTypes return */ + reg::packed(878, 0, 884), /* defaultStaticCallNarrowing $scope */ + reg::packed(765, 0, 770), /* defaultStaticCallNarrowing $expr */ + reg::packed(1133, MAY_BE_NULL, 1020), /* defaultStaticCallNarrowing $classResult */ + reg::packed(977, 0, 1396), /* defaultStaticCallNarrowing $context */ + reg::packed(790, 0, 1439), /* defaultStaticCallNarrowing return */ + reg::packed(878, 0, 884), /* isStaticCallNarrowable $scope */ + reg::packed(765, 0, 770), /* isStaticCallNarrowable $expr */ + reg::packed(1133, MAY_BE_NULL, 1020), /* isStaticCallNarrowable $classResult */ + reg::packed(790, MAY_BE_BOOL), /* isStaticCallNarrowable return */ + reg::packed(878, 0, 884), /* resolveTypeByNameWithLateStaticBinding $scope */ + reg::packed(1534, 0, 1540), /* resolveTypeByNameWithLateStaticBinding $class */ + reg::packed(1560, MAY_BE_STRING), /* resolveTypeByNameWithLateStaticBinding $methodName */ + reg::packed(790, 0, 1571), /* resolveTypeByNameWithLateStaticBinding return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 676 /* __construct */, 10, 0, 10, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig supports = { { 714 /* supports */, 1, 10, 1, 11, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processExpr = { { 977 /* processExpr */, 7, 12, 7, 19, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveReturnType = { { 1194 /* resolveReturnType */, 7, 20, 7, 27, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig specifyTypes = { { 1394 /* specifyTypes */, 7, 28, 8, 36, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig defaultStaticCallNarrowing = { { 1407 /* defaultStaticCallNarrowing */, 4, 37, 4, 41, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isStaticCallNarrowable = { { 1434 /* isStaticCallNarrowable */, 3, 42, 3, 45, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 1525 /* resolveTypeByNameWithLateStaticBinding */, 3, 46, 3, 49, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig __construct = { { 753 /* __construct */, 11, 0, 11, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig supports = { { 791 /* supports */, 1, 11, 1, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processExpr = { { 1054 /* processExpr */, 7, 13, 7, 20, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveReturnType = { { 1271 /* resolveReturnType */, 7, 21, 7, 28, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyTypes = { { 1471 /* specifyTypes */, 7, 29, 8, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig defaultStaticCallNarrowing = { { 1484 /* defaultStaticCallNarrowing */, 4, 38, 4, 42, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isStaticCallNarrowable = { { 1511 /* isStaticCallNarrowable */, 3, 43, 3, 46, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 1602 /* resolveTypeByNameWithLateStaticBinding */, 3, 47, 3, 50, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::StaticCallHandler From 5946ba6eee065e8963799394b5d37d091c4031c3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 4 Nov 2026 13:18:50 +0000 Subject: [PATCH 2/2] Move hasConditionalAsserts() to Assertions Co-Authored-By: Claude Opus 5.5 --- src/Analyser/ExprHandler/FuncCallHandler.php | 2 +- .../ExprHandler/MethodCallHandler.php | 2 +- .../ExprHandler/StaticCallHandler.php | 2 +- src/Reflection/Assertions.php | 12 ++++++ .../Comparison/ImpossibleCheckTypeHelper.php | 8 ---- turbo-ext/src/Assertions.cpp | 40 +++++++++++++++++++ turbo-ext/src/CallHandlerSupport.h | 17 -------- turbo-ext/src/FuncCallHandler.cpp | 2 +- turbo-ext/src/MethodCallHandler.cpp | 2 +- turbo-ext/src/StaticCallHandler.cpp | 2 +- turbo-ext/src/generated/Assertions.h | 5 ++- turbo-ext/src/support.h | 2 + turbo-ext/tests/type-family.php | 1 + 13 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 92f07c66a7a..da37d676f25 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -406,7 +406,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // inside resolveReturnType), the acceptor is re-derived from the // already-processed argument results on the asking scope. $storageRef = WeakReference::create($storage); - $hasConditionalAsserts = $functionReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($functionReflection->getAsserts()); + $hasConditionalAsserts = $functionReflection !== null && $functionReflection->getAsserts()->hasConditionalAsserts(); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) : function (bool $nativeTypesPromoted) use ($nodeScopeResolver, $beforeScope, $expr, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index cc7f0b69616..6c5baf0b2b7 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -220,7 +220,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. $storageRef = WeakReference::create($storage); - $hasConditionalAsserts = $methodReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($methodReflection->getAsserts()); + $hasConditionalAsserts = $methodReflection !== null && $methodReflection->getAsserts()->hasConditionalAsserts(); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) : function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $varResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index c9733a54347..b70204a192d 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -303,7 +303,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // (native-types-promoted, or on-demand / synthetic pricing) the acceptor is // re-derived from the already-processed argument results on the asking scope. $storageRef = WeakReference::create($storage); - $hasConditionalAsserts = $methodReflection !== null && ImpossibleCheckTypeHelper::hasConditionalAsserts($methodReflection->getAsserts()); + $hasConditionalAsserts = $methodReflection !== null && $methodReflection->getAsserts()->hasConditionalAsserts(); $typeCallback = $isEarlyTerminating ? static fn (bool $nativeTypesPromoted): Type => new NeverType(true) : function (bool $nativeTypesPromoted) use ($beforeScope, $expr, $classResult, $nameResult, $resolvedParametersAcceptor, $argsResult, $storageRef, $hasConditionalAsserts): Type { diff --git a/src/Reflection/Assertions.php b/src/Reflection/Assertions.php index 81e9f9a6497..1635b3f2912 100644 --- a/src/Reflection/Assertions.php +++ b/src/Reflection/Assertions.php @@ -196,4 +196,16 @@ public static function createFromAssertTags(array $assertTags): self return self::create($assertTags); } + /** Whether any tag is @phpstan-assert-if-true or @phpstan-assert-if-false. */ + public function hasConditionalAsserts(): bool + { + foreach ($this->asserts as $assert) { + if ($assert->getIf() !== AssertTag::NULL) { + return true; + } + } + + return false; + } + } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 2e278595f7e..875a47111fd 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -15,7 +15,6 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; -use PHPStan\Reflection\Assertions; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; @@ -94,13 +93,6 @@ public function findSpecifiedType( return $specifiedValue; } - public static function hasConditionalAsserts(Assertions $assertions): bool - { - $all = $assertions->getAll(); - - return $all !== [] && count($all) > count($assertions->getAsserts()); - } - /** * Pins a boolean call with @phpstan-assert-if-true/-if-false to true/false when * the argument types already decide the assertion - the same verdict the diff --git a/turbo-ext/src/Assertions.cpp b/turbo-ext/src/Assertions.cpp index 274862f834e..abcec1a876f 100644 --- a/turbo-ext/src/Assertions.cpp +++ b/turbo-ext/src/Assertions.cpp @@ -144,6 +144,29 @@ class Assertions return filterByIf(asserts, "", 0, false, PT_AS_CLOSURE("getAsserts", "58")); } + /* foreach ($this->asserts as $assert) { if ($assert->getIf() !== + * AssertTag::NULL) return true; } return false; — false = pending exception */ + [[nodiscard]] bool hasConditionalAsserts(bool &out) const + { + zval *asserts = slot(); + if (UNEXPECTED(asserts == NULL)) return false; + for (zv::ArrayEntry entry : zv::TableRef(Z_ARRVAL_P(asserts))) { + zval *tag = entry.value().deref().raw(); + if (UNEXPECTED(Z_TYPE_P(tag) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getIf() on %s", zend_zval_value_name(tag)); + return false; + } + bool unconditional; + if (UNEXPECTED(!tagIfIs(tag, "", 0, unconditional))) return false; + if (!unconditional) { + out = true; + return true; + } + } + out = false; + return true; + } + /* array_merge(the IF_TRUE tags, the negated IF_FALSE non-equality tags) */ zv::Val getAssertsIfTrue() const { @@ -465,6 +488,21 @@ zv::Val pt_assertions_get_asserts(zval *assertions, int which) return result; } +bool pt_assertions_has_conditional_asserts(zval *assertions, bool &out) +{ + if (EXPECTED(Z_TYPE_P(assertions) == IS_OBJECT && Z_OBJCE_P(assertions) == pt_ce_assertions)) { + return Assertions(Z_OBJ_P(assertions)).hasConditionalAsserts(out); + } + if (UNEXPECTED(Z_TYPE_P(assertions) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function hasConditionalAsserts() on %s", zend_zval_value_name(assertions)); + return false; + } + zv::Val result = pt_type_call(Z_OBJ_P(assertions), PT_LC("hasconditionalasserts"), 0, NULL); + if (UNEXPECTED(result.isUndef())) return false; + out = zend_is_true(result.raw()); + return true; +} + zval *pt_assertions_all(zval *assertions, zv::Val &hold) { if (EXPECTED(Z_TYPE_P(assertions) == IS_OBJECT && Z_OBJCE_P(assertions) == pt_ce_assertions)) { @@ -579,6 +617,8 @@ PT_MINIT_REGISTRATION(pt_register_assertions) PT_RETURN_VAL(Assertions::create(assertTags)); }); + cls.method<&Assertions::hasConditionalAsserts>(sigs::hasConditionalAsserts); + cls.shadow(&pt_ce_assertions); } diff --git a/turbo-ext/src/CallHandlerSupport.h b/turbo-ext/src/CallHandlerSupport.h index 3ef3632cdf6..dfd47572e66 100644 --- a/turbo-ext/src/CallHandlerSupport.h +++ b/turbo-ext/src/CallHandlerSupport.h @@ -134,23 +134,6 @@ inline zv::Val assertionsGetAll(zval *assertions) return hold.isUndef() ? zv::Val::copyOf(zv::Ref(all)) : std::move(hold); } -/* ImpossibleCheckTypeHelper::hasConditionalAsserts($assertions): whether any - * tag is @phpstan-assert-if-true/-if-false; false = pending exception */ -[[nodiscard]] inline bool hasConditionalAsserts(zval *assertions, bool &out) -{ - zv::Val all = assertionsGetAll(assertions); - if (UNEXPECTED(all.isUndef())) return false; - if (Z_TYPE_P(all.raw()) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(all.raw())) == 0) { - out = false; - return true; - } - zv::Val unconditional = pt_assertions_get_asserts(assertions, PT_ASSERTIONS_UNCONDITIONAL); - if (UNEXPECTED(unconditional.isUndef())) return false; - out = Z_TYPE_P(unconditional.raw()) == IS_ARRAY - && zend_hash_num_elements(Z_ARRVAL_P(all.raw())) > zend_hash_num_elements(Z_ARRVAL_P(unconditional.raw())); - return true; -} - inline pt_method_site resolveConditionalAssertReturnTypeSite; /* $impossibleCheckTypeHelper->resolveConditionalAssertReturnType($scope, $node, diff --git a/turbo-ext/src/FuncCallHandler.cpp b/turbo-ext/src/FuncCallHandler.cpp index b76cc840b94..762b3506612 100644 --- a/turbo-ext/src/FuncCallHandler.cpp +++ b/turbo-ext/src/FuncCallHandler.cpp @@ -1079,7 +1079,7 @@ class FuncCallHandler zv::Val assertsHold; zval *assertions = pt_function_reflection_asserts(functionReflection.raw(), assertsHold); if (UNEXPECTED(assertions == NULL)) return zv::Val(); - if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions, hasConditionalAsserts))) return zv::Val(); + if (UNEXPECTED(!pt_assertions_has_conditional_asserts(assertions, hasConditionalAsserts))) return zv::Val(); } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&neverTypeCallbackBody) diff --git a/turbo-ext/src/MethodCallHandler.cpp b/turbo-ext/src/MethodCallHandler.cpp index 4922255d973..43e7fc22f4d 100644 --- a/turbo-ext/src/MethodCallHandler.cpp +++ b/turbo-ext/src/MethodCallHandler.cpp @@ -401,7 +401,7 @@ class MethodCallHandler if (!methodReflection.isNull()) { zv::Val assertions = pt_extended_method_reflection_call(methodReflection.raw(), PT_MR_GET_ASSERTS); if (UNEXPECTED(assertions.isUndef())) return zv::Val(); - if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); + if (UNEXPECTED(!pt_assertions_has_conditional_asserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&earlyTerminatingTypeCallbackBody) diff --git a/turbo-ext/src/StaticCallHandler.cpp b/turbo-ext/src/StaticCallHandler.cpp index 97b644af493..3cc58166742 100644 --- a/turbo-ext/src/StaticCallHandler.cpp +++ b/turbo-ext/src/StaticCallHandler.cpp @@ -644,7 +644,7 @@ class StaticCallHandler if (!methodReflection.isNull()) { zv::Val assertions = pt_extended_method_reflection_call(methodReflection.raw(), PT_MR_GET_ASSERTS); if (UNEXPECTED(assertions.isUndef())) return zv::Val(); - if (UNEXPECTED(!ptcall::hasConditionalAsserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); + if (UNEXPECTED(!pt_assertions_has_conditional_asserts(assertions.raw(), hasConditionalAsserts))) return zv::Val(); } zv::Val typeCallback = isEarlyTerminating ? pt_native_closure(&earlyTerminatingTypeCallbackBody) diff --git a/turbo-ext/src/generated/Assertions.h b/turbo-ext/src/generated/Assertions.h index 12d1a0fa77d..f1253d8c7e4 100644 --- a/turbo-ext/src/generated/Assertions.h +++ b/turbo-ext/src/generated/Assertions.h @@ -51,7 +51,8 @@ inline constexpr char strings[] = "PHPStan\\PhpDoc\\ResolvedPhpDocBlock\0" /* 238 */ "createFromResolvedPhpDocBlock\0" /* 273 */ "assertTags\0" /* 303 */ - "createFromAssertTags"; /* 314 */ + "createFromAssertTags\0" /* 314 */ + "hasConditionalAsserts"; /* 335 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_ARRAY), /* __construct $asserts */ reg::packed(20, MAY_BE_ARRAY), /* getAll return */ @@ -75,6 +76,7 @@ inline constexpr reg::PackedArg args[] = { reg::packed(20, 0, 83), /* createFromResolvedPhpDocBlock return */ reg::packed(303, MAY_BE_ARRAY), /* createFromAssertTags $assertTags */ reg::packed(20, 0, 83), /* createFromAssertTags return */ + reg::packed(20, MAY_BE_BOOL), /* hasConditionalAsserts return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -95,6 +97,7 @@ inline constexpr sigtab::Sig create = { { 207 /* create */, 1, 15, 1, 16, ZEND_A inline constexpr sigtab::Sig createEmpty = { { 214 /* createEmpty */, 0, 17, 0, 17, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig createFromResolvedPhpDocBlock = { { 273 /* createFromResolvedPhpDocBlock */, 1, 18, 1, 19, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; inline constexpr sigtab::Sig createFromAssertTags = { { 314 /* createFromAssertTags */, 1, 20, 1, 21, ZEND_ACC_PUBLIC | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig hasConditionalAsserts = { { 335 /* hasConditionalAsserts */, 0, 22, 0, 22, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::Assertions diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 08a1d65da76..e5a0b0a06a5 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -3940,6 +3940,8 @@ zv::Val pt_assertions_map_types(zval *assertions, zval *callable); #define PT_ASSERTIONS_IF_TRUE 1 #define PT_ASSERTIONS_IF_FALSE 2 zv::Val pt_assertions_get_asserts(zval *assertions, int which); +/* $assertions->hasConditionalAsserts(); false = pending exception */ +[[nodiscard]] bool pt_assertions_has_conditional_asserts(zval *assertions, bool &out); /* }}} */ diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index c3912a633e9..b25dcaab6d3 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -7564,6 +7564,7 @@ public function getResolvedPhpDoc(): ?\PHPStan\PhpDoc\ResolvedPhpDocBlock { retu 'asserts' => $catching(static fn () => $viewTags($a->getAsserts())), 'ifTrue' => $catching(static fn () => $viewTags($a->getAssertsIfTrue())), 'ifFalse' => $catching(static fn () => $viewTags($a->getAssertsIfFalse())), + 'hasConditionalAsserts' => $catching(static fn () => $a->hasConditionalAsserts()), 'isEmpty' => $a === \PHPStan\Reflection\Assertions::createEmpty(), ]; };