Skip to content

Commit 7a57c23

Browse files
phpstan-botclaude
andcommitted
Generalize NeverType handling for member access in MutatingScope
Move the NeverType-instead-of-ErrorType fix from individual handlers (PropertyFetchHandler, MethodCallHandler) to MutatingScope::resolveType(). This provides a single central fix: when any handler returns ErrorType for a member access on NeverType (dead code branch after type narrowing), it is converted to NeverType. This ensures TypeCombinator::union() correctly eliminates the dead branch type instead of polluting the union with mixed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 79bbb0c commit 7a57c23

3 files changed

Lines changed: 21 additions & 12 deletions

File tree

src/Analyser/ExprHandler/MethodCallHandler.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,6 @@ private function getMethodThrowPoint(MethodReflection $methodReflection, Paramet
289289

290290
public function resolveType(MutatingScope $scope, Expr $expr): Type
291291
{
292-
$calledOnType = $scope->getType($expr->var);
293-
if ($calledOnType instanceof NeverType) {
294-
return new NeverType();
295-
}
296-
297292
if ($expr->name instanceof Identifier) {
298293
if ($scope->nativeTypesPromoted) {
299294
$methodReflection = $scope->getMethodReflection(

src/Analyser/ExprHandler/PropertyFetchHandler.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
use PHPStan\Rules\Properties\PropertyReflectionFinder;
2121
use PHPStan\Type\ErrorType;
2222
use PHPStan\Type\MixedType;
23-
use PHPStan\Type\NeverType;
2423
use PHPStan\Type\Type;
2524
use PHPStan\Type\TypeCombinator;
2625
use function array_map;
@@ -91,11 +90,6 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
9190

9291
public function resolveType(MutatingScope $scope, Expr $expr): Type
9392
{
94-
$fetchedOnType = $scope->getType($expr->var);
95-
if ($fetchedOnType instanceof NeverType) {
96-
return new NeverType();
97-
}
98-
9993
if ($expr->name instanceof Identifier) {
10094
if ($scope->nativeTypesPromoted) {
10195
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $scope);

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,12 +982,32 @@ private function resolveType(string $exprString, Expr $node): Type
982982
continue;
983983
}
984984

985-
return $exprHandler->resolveType($this, $node);
985+
$type = $exprHandler->resolveType($this, $node);
986+
987+
// When a member access is performed on NeverType (e.g. in a dead code
988+
// branch after type narrowing), the handler can't find the member and
989+
// returns ErrorType. Since ErrorType extends MixedType, it pollutes
990+
// type unions (e.g. union(ErrorType, string) = mixed instead of string).
991+
// The correct result is NeverType, which is eliminated from unions.
992+
if ($type instanceof ErrorType && $this->resolvedMemberAccessOnNeverType($node)) {
993+
return new NeverType();
994+
}
995+
996+
return $type;
986997
}
987998

988999
return new MixedType();
9891000
}
9901001

1002+
private function resolvedMemberAccessOnNeverType(Expr $node): bool
1003+
{
1004+
if ($node instanceof PropertyFetch || $node instanceof MethodCall) {
1005+
return $this->getType($node->var) instanceof NeverType;
1006+
}
1007+
1008+
return false;
1009+
}
1010+
9911011
/**
9921012
* @param callable(Type): ?bool $typeCallback
9931013
*/

0 commit comments

Comments
 (0)