Skip to content

Commit db849fa

Browse files
staabmphpstan-bot
authored andcommitted
Fix phpstan/phpstan#13566: False positive staticMethod.impossibleType for conditional return type with void/never
- Fixed ImpossibleCheckTypeHelper to skip impossible check when rootExpr is a call argument in null (void) context - The conditional return type ($exit is true ? never : void) describes behavioral control flow, not a type check - Added regression test in tests/PHPStan/Rules/Comparison/data/bug-13566.php - The root cause was that getConditionalSpecifiedTypes set rootExpr to the call argument via inner specifyTypesInCondition, causing findSpecifiedType to short-circuit with a false "always evaluates to false" result
1 parent 925f29c commit db849fa

3 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/Rules/Comparison/ImpossibleCheckTypeHelper.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ public function findSpecifiedType(
252252
}
253253

254254
$typeSpecifierScope = $this->treatPhpDocTypesAsCertain ? $scope : $scope->doNotTreatPhpDocTypesAsCertain();
255-
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($typeSpecifierScope, $node, $this->determineContext($typeSpecifierScope, $node));
255+
$context = $this->determineContext($typeSpecifierScope, $node);
256+
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($typeSpecifierScope, $node, $context);
256257

257258
// don't validate types on overwrite
258259
if ($specifiedTypes->shouldOverwrite()) {
@@ -268,6 +269,17 @@ public function findSpecifiedType(
268269
return null;
269270
}
270271

272+
// For void methods (null context), if the rootExpr is one of the call's arguments,
273+
// the conditional return type describes behavioral control flow (e.g. void vs never
274+
// based on a boolean flag), not a type check. Skip the impossible check.
275+
if ($context->null() && $node instanceof Expr\CallLike) {
276+
foreach ($node->getArgs() as $arg) {
277+
if ($arg->value === $rootExpr) {
278+
return null;
279+
}
280+
}
281+
}
282+
271283
$rootExprType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($rootExpr) : $scope->getNativeType($rootExpr));
272284
if ($rootExprType instanceof ConstantBooleanType) {
273285
return $rootExprType->getValue();

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ public function testBug12087b(): void
161161
]);
162162
}
163163

164+
public function testBug13566(): void
165+
{
166+
$this->treatPhpDocTypesAsCertain = true;
167+
$this->analyse([__DIR__ . '/data/bug-13566.php'], []);
168+
}
169+
164170
public static function getAdditionalConfigFiles(): array
165171
{
166172
return [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13566;
4+
5+
class HelloWorld
6+
{
7+
/** @return ($exit is true ? never : void) */
8+
public static function notFound(bool $exit = true): void
9+
{
10+
header('HTTP/1.1 404 Not Found', true, 404);
11+
12+
if ($exit) {
13+
echo '404 Not Found';
14+
exit;
15+
}
16+
}
17+
18+
public function test(): void
19+
{
20+
// send 404 header without exiting
21+
self::notFound(false);
22+
}
23+
}

0 commit comments

Comments
 (0)