|
57 | 57 | use PHPStan\Type\FloatType; |
58 | 58 | use PHPStan\Type\FunctionTypeSpecifyingExtension; |
59 | 59 | use PHPStan\Type\Generic\GenericClassStringType; |
| 60 | +use PHPStan\Type\Generic\GenericObjectType; |
60 | 61 | use PHPStan\Type\Generic\TemplateType; |
61 | 62 | use PHPStan\Type\Generic\TemplateTypeHelper; |
62 | 63 | use PHPStan\Type\Generic\TemplateTypeVariance; |
|
86 | 87 | use function array_merge; |
87 | 88 | use function array_reverse; |
88 | 89 | use function array_shift; |
| 90 | +use function array_values; |
89 | 91 | use function count; |
90 | 92 | use function in_array; |
91 | 93 | use function is_string; |
@@ -2577,6 +2579,48 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty |
2577 | 2579 | return $specifiedTypes; |
2578 | 2580 | } |
2579 | 2581 |
|
| 2582 | + private function narrowTypePreservingGenerics(ObjectType $narrowedType, Type $currentVarType): Type |
| 2583 | + { |
| 2584 | + // Intersect with current type — for unions (e.g., Cat<string>|Dog<string>), |
| 2585 | + // TypeCombinator distributes over members and preserves generic parameters |
| 2586 | + $intersected = TypeCombinator::intersect($narrowedType, $currentVarType); |
| 2587 | + if (!$intersected instanceof NeverType && !$intersected->equals($narrowedType)) { |
| 2588 | + return $intersected; |
| 2589 | + } |
| 2590 | + |
| 2591 | + // For generic parent types (e.g., Animal<string>), resolve child's |
| 2592 | + // template parameters through the class hierarchy |
| 2593 | + $currentClassReflections = $currentVarType->getObjectClassReflections(); |
| 2594 | + if ( |
| 2595 | + count($currentClassReflections) === 1 |
| 2596 | + && count($currentClassReflections[0]->getTemplateTypeMap()->getTypes()) > 0 |
| 2597 | + ) { |
| 2598 | + $className = $narrowedType->getClassName(); |
| 2599 | + if ($this->reflectionProvider->hasClass($className)) { |
| 2600 | + $childReflection = $this->reflectionProvider->getClass($className); |
| 2601 | + $childTemplateTypes = $childReflection->getTemplateTypeMap()->getTypes(); |
| 2602 | + if (count($childTemplateTypes) > 0) { |
| 2603 | + $freshChildType = new GenericObjectType($className, array_values($childTemplateTypes)); |
| 2604 | + $currentClassNames = $currentVarType->getObjectClassNames(); |
| 2605 | + $ancestorAsParent = count($currentClassNames) === 1 |
| 2606 | + ? $freshChildType->getAncestorWithClassName($currentClassNames[0]) |
| 2607 | + : null; |
| 2608 | + if ($ancestorAsParent !== null) { |
| 2609 | + $inferredMap = $ancestorAsParent->inferTemplateTypes($currentVarType); |
| 2610 | + $resolvedTypes = []; |
| 2611 | + foreach ($childTemplateTypes as $name => $templateType) { |
| 2612 | + $resolved = $inferredMap->getType($name); |
| 2613 | + $resolvedTypes[] = $resolved ?? $templateType; |
| 2614 | + } |
| 2615 | + return new GenericObjectType($className, $resolvedTypes); |
| 2616 | + } |
| 2617 | + } |
| 2618 | + } |
| 2619 | + } |
| 2620 | + |
| 2621 | + return $narrowedType; |
| 2622 | + } |
| 2623 | + |
2580 | 2624 | private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes |
2581 | 2625 | { |
2582 | 2626 | $leftExpr = $expr->left; |
@@ -2879,9 +2923,16 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope |
2879 | 2923 | strtolower($unwrappedLeftExpr->name->toString()) === 'class' |
2880 | 2924 | ) { |
2881 | 2925 | if ($this->reflectionProvider->hasClass($rightType->getValue())) { |
| 2926 | + $className = $rightType->getValue(); |
| 2927 | + $classReflection = $this->reflectionProvider->getClass($className); |
| 2928 | + $narrowedType = new ObjectType($className, classReflection: $classReflection->asFinal()); |
| 2929 | + |
| 2930 | + $currentVarType = $scope->getType($unwrappedLeftExpr->class); |
| 2931 | + $narrowedType = $this->narrowTypePreservingGenerics($narrowedType, $currentVarType); |
| 2932 | + |
2882 | 2933 | return $this->create( |
2883 | 2934 | $unwrappedLeftExpr->class, |
2884 | | - new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()), |
| 2935 | + $narrowedType, |
2885 | 2936 | $context, |
2886 | 2937 | $scope, |
2887 | 2938 | )->unionWith($this->create($leftExpr, $rightType, $context, $scope))->setRootExpr($expr); |
@@ -2910,9 +2961,16 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope |
2910 | 2961 | strtolower($unwrappedRightExpr->name->toString()) === 'class' |
2911 | 2962 | ) { |
2912 | 2963 | if ($this->reflectionProvider->hasClass($leftType->getValue())) { |
| 2964 | + $className = $leftType->getValue(); |
| 2965 | + $classReflection = $this->reflectionProvider->getClass($className); |
| 2966 | + $narrowedType = new ObjectType($className, classReflection: $classReflection->asFinal()); |
| 2967 | + |
| 2968 | + $currentVarType = $scope->getType($unwrappedRightExpr->class); |
| 2969 | + $narrowedType = $this->narrowTypePreservingGenerics($narrowedType, $currentVarType); |
| 2970 | + |
2913 | 2971 | return $this->create( |
2914 | 2972 | $unwrappedRightExpr->class, |
2915 | | - new ObjectType($leftType->getValue(), classReflection: $this->reflectionProvider->getClass($leftType->getValue())->asFinal()), |
| 2973 | + $narrowedType, |
2916 | 2974 | $context, |
2917 | 2975 | $scope, |
2918 | 2976 | )->unionWith($this->create($rightExpr, $leftType, $context, $scope)->setRootExpr($expr)); |
|
0 commit comments