Skip to content

Commit 743670c

Browse files
phpstan-botondrejmirtes
authored andcommitted
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.
1 parent 5ab83ce commit 743670c

File tree

6 files changed

+81
-0
lines changed

6 files changed

+81
-0
lines changed

src/Type/Generic/TemplateBenevolentUnionType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type\Generic;
44

55
use PHPStan\Type\BenevolentUnionType;
6+
use PHPStan\Type\NeverType;
67
use PHPStan\Type\Type;
78

89
/** @api */
@@ -50,6 +51,9 @@ public function withTypes(array $types): self
5051
public function filterTypes(callable $filterCb): Type
5152
{
5253
$result = parent::filterTypes($filterCb);
54+
if ($result instanceof NeverType) {
55+
return $result;
56+
}
5357
if (!$result instanceof TemplateType) {
5458
return TemplateTypeFactory::create(
5559
$this->getScope(),

src/Type/Generic/TemplateUnionType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Type\Generic;
44

5+
use PHPStan\Type\NeverType;
56
use PHPStan\Type\Type;
67
use PHPStan\Type\UnionType;
78

@@ -37,6 +38,9 @@ public function __construct(
3738
public function filterTypes(callable $filterCb): Type
3839
{
3940
$result = parent::filterTypes($filterCb);
41+
if ($result instanceof NeverType) {
42+
return $result;
43+
}
4044
if (!$result instanceof TemplateType) {
4145
return TemplateTypeFactory::create(
4246
$this->getScope(),

tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,9 @@ public function testBug12119(): void
209209
$this->analyse([__DIR__ . '/data/bug-12119.php'], []);
210210
}
211211

212+
public function testBug14504(): void
213+
{
214+
$this->analyse([__DIR__ . '/data/bug-14504.php'], []);
215+
}
216+
212217
}

tests/PHPStan/Rules/Pure/PureMethodRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,10 @@ public function testBug12382(): void
377377
]);
378378
}
379379

380+
public function testBug14504(): void
381+
{
382+
$this->treatPhpDocTypesAsCertain = true;
383+
$this->analyse([__DIR__ . '/data/bug-14504-method.php'], []);
384+
}
385+
380386
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14504Method;
4+
5+
/**
6+
* @template T of int|string
7+
*/
8+
class Foo
9+
{
10+
11+
/** @param T $val */
12+
public function __construct(private $val) {}
13+
14+
/** @phpstan-pure */
15+
public function toString(): string {
16+
return (string)$this->val;
17+
}
18+
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14504;
4+
5+
/**
6+
* @phpstan-pure
7+
* @template T of int|string
8+
* @param T $val
9+
* @return string
10+
*/
11+
function stringCast($val): string {
12+
return (string)$val;
13+
}
14+
15+
/**
16+
* @phpstan-pure
17+
* @template T of int|string
18+
* @param T $val
19+
* @return string
20+
*/
21+
function stringConcat($val): string {
22+
return '' . $val;
23+
}
24+
25+
/**
26+
* @phpstan-pure
27+
* @template T of int|string
28+
* @param T $val
29+
* @return string
30+
*/
31+
function stringInterpolation($val): string {
32+
return "$val";
33+
}
34+
35+
/**
36+
* @phpstan-pure
37+
* @template T of int|float|bool
38+
* @param T $val
39+
* @return string
40+
*/
41+
function nonStringNonObjectCast($val): string {
42+
return (string)$val;
43+
}

0 commit comments

Comments
 (0)