From 4cb31074e6b49013f6a0850d2e92a74aee3b0130 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 21 Apr 2026 05:27:00 +0000 Subject: [PATCH] Do not re-wrap `NeverType` as `TemplateMixedType` in `TemplateUnionType::filterTypes()` - When TemplateUnionType::filterTypes() filters out all inner types, the parent returns NeverType. The override was re-wrapping it via TemplateTypeFactory::create(), which has no NeverType branch and falls through to TemplateMixedType. - This caused MixedType::hasMethod() to return Yes for __toString, leading to a false "Possibly impure call to method stdClass::__toString()" on pure functions casting template types like T of int|string to string. - Applied the same fix to TemplateBenevolentUnionType::filterTypes(). - The fix covers all callers of filterTypes: filterTypeWithMethod (string cast, concatenation, interpolation, echo, print), filterTypeWithProperty, filterTypeWithConstant, and filterTypeWhenIterable. --- .../Generic/TemplateBenevolentUnionType.php | 4 ++ src/Type/Generic/TemplateUnionType.php | 4 ++ .../Rules/Pure/PureFunctionRuleTest.php | 5 +++ .../PHPStan/Rules/Pure/PureMethodRuleTest.php | 6 +++ .../Rules/Pure/data/bug-14504-method.php | 19 ++++++++ tests/PHPStan/Rules/Pure/data/bug-14504.php | 43 +++++++++++++++++++ 6 files changed, 81 insertions(+) create mode 100644 tests/PHPStan/Rules/Pure/data/bug-14504-method.php create mode 100644 tests/PHPStan/Rules/Pure/data/bug-14504.php diff --git a/src/Type/Generic/TemplateBenevolentUnionType.php b/src/Type/Generic/TemplateBenevolentUnionType.php index c8c2caaefdf..d9e1a100267 100644 --- a/src/Type/Generic/TemplateBenevolentUnionType.php +++ b/src/Type/Generic/TemplateBenevolentUnionType.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Generic; use PHPStan\Type\BenevolentUnionType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; /** @api */ @@ -50,6 +51,9 @@ public function withTypes(array $types): self public function filterTypes(callable $filterCb): Type { $result = parent::filterTypes($filterCb); + if ($result instanceof NeverType) { + return $result; + } if (!$result instanceof TemplateType) { return TemplateTypeFactory::create( $this->getScope(), diff --git a/src/Type/Generic/TemplateUnionType.php b/src/Type/Generic/TemplateUnionType.php index dc58af565aa..6a9e7f49477 100644 --- a/src/Type/Generic/TemplateUnionType.php +++ b/src/Type/Generic/TemplateUnionType.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Generic; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\UnionType; @@ -37,6 +38,9 @@ public function __construct( public function filterTypes(callable $filterCb): Type { $result = parent::filterTypes($filterCb); + if ($result instanceof NeverType) { + return $result; + } if (!$result instanceof TemplateType) { return TemplateTypeFactory::create( $this->getScope(), diff --git a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php index 9ae7657ddbc..9fa246f5552 100644 --- a/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php @@ -209,4 +209,9 @@ public function testBug12119(): void $this->analyse([__DIR__ . '/data/bug-12119.php'], []); } + public function testBug14504(): void + { + $this->analyse([__DIR__ . '/data/bug-14504.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php index 7a6a0ae9f57..2b896c17b10 100644 --- a/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php +++ b/tests/PHPStan/Rules/Pure/PureMethodRuleTest.php @@ -377,4 +377,10 @@ public function testBug12382(): void ]); } + public function testBug14504(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14504-method.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Pure/data/bug-14504-method.php b/tests/PHPStan/Rules/Pure/data/bug-14504-method.php new file mode 100644 index 00000000000..acbd43de8e4 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-14504-method.php @@ -0,0 +1,19 @@ +val; + } + +} diff --git a/tests/PHPStan/Rules/Pure/data/bug-14504.php b/tests/PHPStan/Rules/Pure/data/bug-14504.php new file mode 100644 index 00000000000..f4192a3c314 --- /dev/null +++ b/tests/PHPStan/Rules/Pure/data/bug-14504.php @@ -0,0 +1,43 @@ +